1 // 4.0.25 (2014-04-30) 2 3 /** 4 * Compiled inline version. (Library mode) 5 */ 6 7 /*jshint smarttabs:true, undef:true, latedef:true, curly:true, bitwise:true, camelcase:true */ 8 /*globals $code */ 9 10 (function(exports, undefined) { 11 "use strict"; 12 13 var modules = {}; 14 15 function require(ids, callback) { 16 var module, defs = []; 17 18 for (var i = 0; i < ids.length; ++i) { 19 module = modules[ids[i]] || resolve(ids[i]); 20 if (!module) { 21 throw 'module definition dependecy not found: ' + ids[i]; 22 } 23 24 defs.push(module); 25 } 26 27 callback.apply(null, defs); 28 } 29 30 function define(id, dependencies, definition) { 31 if (typeof id !== 'string') { 32 throw 'invalid module definition, module id must be defined and be a string'; 33 } 34 35 if (dependencies === undefined) { 36 throw 'invalid module definition, dependencies must be specified'; 37 } 38 39 if (definition === undefined) { 40 throw 'invalid module definition, definition function must be specified'; 41 } 42 43 require(dependencies, function() { 44 modules[id] = definition.apply(null, arguments); 45 }); 46 } 47 48 function defined(id) { 49 return !!modules[id]; 50 } 51 52 function resolve(id) { 53 var target = exports; 54 var fragments = id.split(/[.\/]/); 55 56 for (var fi = 0; fi < fragments.length; ++fi) { 57 if (!target[fragments[fi]]) { 58 return; 59 } 60 61 target = target[fragments[fi]]; 62 } 63 64 return target; 65 } 66 67 function expose(ids) { 68 for (var i = 0; i < ids.length; i++) { 69 var target = exports; 70 var id = ids[i]; 71 var fragments = id.split(/[.\/]/); 72 73 for (var fi = 0; fi < fragments.length - 1; ++fi) { 74 if (target[fragments[fi]] === undefined) { 75 target[fragments[fi]] = {}; 76 } 77 78 target = target[fragments[fi]]; 79 } 80 81 target[fragments[fragments.length - 1]] = modules[id]; 82 } 83 } 84 85 // Included from: js/tinymce/classes/dom/Sizzle.jQuery.js 86 87 /** 88 * Sizzle.jQuery.js 89 * 90 * Copyright, Moxiecode Systems AB 91 * Released under LGPL License. 92 * 93 * License: http://www.tinymce.com/license 94 * Contributing: http://www.tinymce.com/contributing 95 */ 96 97 /*global jQuery:true */ 98 99 /* 100 * Fake Sizzle using jQuery. 101 */ 102 define("tinymce/dom/Sizzle", [], function() { 103 // Detect if jQuery is loaded 104 if (!window.jQuery) { 105 throw new Error("Load jQuery first"); 106 } 107 108 return jQuery.find; 109 }); 110 111 // Included from: js/tinymce/classes/html/Styles.js 112 113 /** 114 * Styles.js 115 * 116 * Copyright, Moxiecode Systems AB 117 * Released under LGPL License. 118 * 119 * License: http://www.tinymce.com/license 120 * Contributing: http://www.tinymce.com/contributing 121 */ 122 123 /** 124 * This class is used to parse CSS styles it also compresses styles to reduce the output size. 125 * 126 * @example 127 * var Styles = new tinymce.html.Styles({ 128 * url_converter: function(url) { 129 * return url; 130 * } 131 * }); 132 * 133 * styles = Styles.parse('border: 1px solid red'); 134 * styles.color = 'red'; 135 * 136 * console.log(new tinymce.html.StyleSerializer().serialize(styles)); 137 * 138 * @class tinymce.html.Styles 139 * @version 3.4 140 */ 141 define("tinymce/html/Styles", [], function() { 142 return function(settings, schema) { 143 /*jshint maxlen:255 */ 144 /*eslint max-len:0 */ 145 var rgbRegExp = /rgb\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*\)/gi, 146 urlOrStrRegExp = /(?:url(?:(?:\(\s*\"([^\"]+)\"\s*\))|(?:\(\s*\'([^\']+)\'\s*\))|(?:\(\s*([^)\s]+)\s*\))))|(?:\'([^\']+)\')|(?:\"([^\"]+)\")/gi, 147 styleRegExp = /\s*([^:]+):\s*([^;]+);?/g, 148 trimRightRegExp = /\s+$/, 149 undef, i, encodingLookup = {}, encodingItems, invisibleChar = '\uFEFF'; 150 151 settings = settings || {}; 152 153 encodingItems = ('\\" \\\' \\; \\: ; : ' + invisibleChar).split(' '); 154 for (i = 0; i < encodingItems.length; i++) { 155 encodingLookup[encodingItems[i]] = invisibleChar + i; 156 encodingLookup[invisibleChar + i] = encodingItems[i]; 157 } 158 159 function toHex(match, r, g, b) { 160 function hex(val) { 161 val = parseInt(val, 10).toString(16); 162 163 return val.length > 1 ? val : '0' + val; // 0 -> 00 164 } 165 166 return '#' + hex(r) + hex(g) + hex(b); 167 } 168 169 return { 170 /** 171 * Parses the specified RGB color value and returns a hex version of that color. 172 * 173 * @method toHex 174 * @param {String} color RGB string value like rgb(1,2,3) 175 * @return {String} Hex version of that RGB value like #FF00FF. 176 */ 177 toHex: function(color) { 178 return color.replace(rgbRegExp, toHex); 179 }, 180 181 /** 182 * Parses the specified style value into an object collection. This parser will also 183 * merge and remove any redundant items that browsers might have added. It will also convert non hex 184 * colors to hex values. Urls inside the styles will also be converted to absolute/relative based on settings. 185 * 186 * @method parse 187 * @param {String} css Style value to parse for example: border:1px solid red;. 188 * @return {Object} Object representation of that style like {border: '1px solid red'} 189 */ 190 parse: function(css) { 191 var styles = {}, matches, name, value, isEncoded, urlConverter = settings.url_converter; 192 var urlConverterScope = settings.url_converter_scope || this; 193 194 function compress(prefix, suffix, noJoin) { 195 var top, right, bottom, left; 196 197 top = styles[prefix + '-top' + suffix]; 198 if (!top) { 199 return; 200 } 201 202 right = styles[prefix + '-right' + suffix]; 203 if (!right) { 204 return; 205 } 206 207 bottom = styles[prefix + '-bottom' + suffix]; 208 if (!bottom) { 209 return; 210 } 211 212 left = styles[prefix + '-left' + suffix]; 213 if (!left) { 214 return; 215 } 216 217 var box = [top, right, bottom, left]; 218 i = box.length - 1; 219 while (i--) { 220 if (box[i] !== box[i + 1]) { 221 break; 222 } 223 } 224 225 if (i > -1 && noJoin) { 226 return; 227 } 228 229 styles[prefix + suffix] = i == -1 ? box[0] : box.join(' '); 230 delete styles[prefix + '-top' + suffix]; 231 delete styles[prefix + '-right' + suffix]; 232 delete styles[prefix + '-bottom' + suffix]; 233 delete styles[prefix + '-left' + suffix]; 234 } 235 236 /** 237 * Checks if the specific style can be compressed in other words if all border-width are equal. 238 */ 239 function canCompress(key) { 240 var value = styles[key], i; 241 242 if (!value) { 243 return; 244 } 245 246 value = value.split(' '); 247 i = value.length; 248 while (i--) { 249 if (value[i] !== value[0]) { 250 return false; 251 } 252 } 253 254 styles[key] = value[0]; 255 256 return true; 257 } 258 259 /** 260 * Compresses multiple styles into one style. 261 */ 262 function compress2(target, a, b, c) { 263 if (!canCompress(a)) { 264 return; 265 } 266 267 if (!canCompress(b)) { 268 return; 269 } 270 271 if (!canCompress(c)) { 272 return; 273 } 274 275 // Compress 276 styles[target] = styles[a] + ' ' + styles[b] + ' ' + styles[c]; 277 delete styles[a]; 278 delete styles[b]; 279 delete styles[c]; 280 } 281 282 // Encodes the specified string by replacing all \" \' ; : with _<num> 283 function encode(str) { 284 isEncoded = true; 285 286 return encodingLookup[str]; 287 } 288 289 // Decodes the specified string by replacing all _<num> with it's original value \" \' etc 290 // It will also decode the \" \' if keep_slashes is set to fale or omitted 291 function decode(str, keep_slashes) { 292 if (isEncoded) { 293 str = str.replace(/\uFEFF[0-9]/g, function(str) { 294 return encodingLookup[str]; 295 }); 296 } 297 298 if (!keep_slashes) { 299 str = str.replace(/\\([\'\";:])/g, "$1"); 300 } 301 302 return str; 303 } 304 305 function processUrl(match, url, url2, url3, str, str2) { 306 str = str || str2; 307 308 if (str) { 309 str = decode(str); 310 311 // Force strings into single quote format 312 return "'" + str.replace(/\'/g, "\\'") + "'"; 313 } 314 315 url = decode(url || url2 || url3); 316 317 if (!settings.allow_script_urls) { 318 var scriptUrl = url.replace(/[\s\r\n]+/, ''); 319 320 if (/(java|vb)script:/i.test(scriptUrl)) { 321 return ""; 322 } 323 324 if (!settings.allow_svg_data_urls && /^data:image\/svg/i.test(scriptUrl)) { 325 return ""; 326 } 327 } 328 329 // Convert the URL to relative/absolute depending on config 330 if (urlConverter) { 331 url = urlConverter.call(urlConverterScope, url, 'style'); 332 } 333 334 // Output new URL format 335 return "url('" + url.replace(/\'/g, "\\'") + "')"; 336 } 337 338 if (css) { 339 css = css.replace(/[\u0000-\u001F]/g, ''); 340 341 // Encode \" \' % and ; and : inside strings so they don't interfere with the style parsing 342 css = css.replace(/\\[\"\';:\uFEFF]/g, encode).replace(/\"[^\"]+\"|\'[^\']+\'/g, function(str) { 343 return str.replace(/[;:]/g, encode); 344 }); 345 346 // Parse styles 347 while ((matches = styleRegExp.exec(css))) { 348 name = matches[1].replace(trimRightRegExp, '').toLowerCase(); 349 value = matches[2].replace(trimRightRegExp, ''); 350 351 // Decode escaped sequences like \65 -> e 352 /*jshint loopfunc:true*/ 353 /*eslint no-loop-func:0 */ 354 value = value.replace(/\\[0-9a-f]+/g, function(e) { 355 return String.fromCharCode(parseInt(e.substr(1), 16)); 356 }); 357 358 if (name && value.length > 0) { 359 // Don't allow behavior name or expression/comments within the values 360 if (!settings.allow_script_urls && (name == "behavior" || /expression\s*\(|\/\*|\*\//.test(value))) { 361 continue; 362 } 363 364 // Opera will produce 700 instead of bold in their style values 365 if (name === 'font-weight' && value === '700') { 366 value = 'bold'; 367 } else if (name === 'color' || name === 'background-color') { // Lowercase colors like RED 368 value = value.toLowerCase(); 369 } 370 371 // Convert RGB colors to HEX 372 value = value.replace(rgbRegExp, toHex); 373 374 // Convert URLs and force them into url('value') format 375 value = value.replace(urlOrStrRegExp, processUrl); 376 styles[name] = isEncoded ? decode(value, true) : value; 377 } 378 379 styleRegExp.lastIndex = matches.index + matches[0].length; 380 } 381 // Compress the styles to reduce it's size for example IE will expand styles 382 compress("border", "", true); 383 compress("border", "-width"); 384 compress("border", "-color"); 385 compress("border", "-style"); 386 compress("padding", ""); 387 compress("margin", ""); 388 compress2('border', 'border-width', 'border-style', 'border-color'); 389 390 // Remove pointless border, IE produces these 391 if (styles.border === 'medium none') { 392 delete styles.border; 393 } 394 395 // IE 11 will produce a border-image: none when getting the style attribute from <p style="border: 1px solid red"></p> 396 // So lets asume it shouldn't be there 397 if (styles['border-image'] === 'none') { 398 delete styles['border-image']; 399 } 400 } 401 402 return styles; 403 }, 404 405 /** 406 * Serializes the specified style object into a string. 407 * 408 * @method serialize 409 * @param {Object} styles Object to serialize as string for example: {border: '1px solid red'} 410 * @param {String} element_name Optional element name, if specified only the styles that matches the schema will be serialized. 411 * @return {String} String representation of the style object for example: border: 1px solid red. 412 */ 413 serialize: function(styles, element_name) { 414 var css = '', name, value; 415 416 function serializeStyles(name) { 417 var styleList, i, l, value; 418 419 styleList = schema.styles[name]; 420 if (styleList) { 421 for (i = 0, l = styleList.length; i < l; i++) { 422 name = styleList[i]; 423 value = styles[name]; 424 425 if (value !== undef && value.length > 0) { 426 css += (css.length > 0 ? ' ' : '') + name + ': ' + value + ';'; 427 } 428 } 429 } 430 } 431 432 // Serialize styles according to schema 433 if (element_name && schema && schema.styles) { 434 // Serialize global styles and element specific styles 435 serializeStyles('*'); 436 serializeStyles(element_name); 437 } else { 438 // Output the styles in the order they are inside the object 439 for (name in styles) { 440 value = styles[name]; 441 442 if (value !== undef && value.length > 0) { 443 css += (css.length > 0 ? ' ' : '') + name + ': ' + value + ';'; 444 } 445 } 446 } 447 448 return css; 449 } 450 }; 451 }; 452 }); 453 454 // Included from: js/tinymce/classes/dom/EventUtils.js 455 456 /** 457 * EventUtils.js 458 * 459 * Copyright, Moxiecode Systems AB 460 * Released under LGPL License. 461 * 462 * License: http://www.tinymce.com/license 463 * Contributing: http://www.tinymce.com/contributing 464 */ 465 466 /*jshint loopfunc:true*/ 467 /*eslint no-loop-func:0 */ 468 469 define("tinymce/dom/EventUtils", [], function() { 470 "use strict"; 471 472 var eventExpandoPrefix = "mce-data-"; 473 var mouseEventRe = /^(?:mouse|contextmenu)|click/; 474 var deprecated = {keyLocation: 1, layerX: 1, layerY: 1, returnValue: 1}; 475 476 /** 477 * Binds a native event to a callback on the speified target. 478 */ 479 function addEvent(target, name, callback, capture) { 480 if (target.addEventListener) { 481 target.addEventListener(name, callback, capture || false); 482 } else if (target.attachEvent) { 483 target.attachEvent('on' + name, callback); 484 } 485 } 486 487 /** 488 * Unbinds a native event callback on the specified target. 489 */ 490 function removeEvent(target, name, callback, capture) { 491 if (target.removeEventListener) { 492 target.removeEventListener(name, callback, capture || false); 493 } else if (target.detachEvent) { 494 target.detachEvent('on' + name, callback); 495 } 496 } 497 498 /** 499 * Normalizes a native event object or just adds the event specific methods on a custom event. 500 */ 501 function fix(originalEvent, data) { 502 var name, event = data || {}, undef; 503 504 // Dummy function that gets replaced on the delegation state functions 505 function returnFalse() { 506 return false; 507 } 508 509 // Dummy function that gets replaced on the delegation state functions 510 function returnTrue() { 511 return true; 512 } 513 514 // Copy all properties from the original event 515 for (name in originalEvent) { 516 // layerX/layerY is deprecated in Chrome and produces a warning 517 if (!deprecated[name]) { 518 event[name] = originalEvent[name]; 519 } 520 } 521 522 // Normalize target IE uses srcElement 523 if (!event.target) { 524 event.target = event.srcElement || document; 525 } 526 527 // Calculate pageX/Y if missing and clientX/Y available 528 if (originalEvent && mouseEventRe.test(originalEvent.type) && originalEvent.pageX === undef && originalEvent.clientX !== undef) { 529 var eventDoc = event.target.ownerDocument || document; 530 var doc = eventDoc.documentElement; 531 var body = eventDoc.body; 532 533 event.pageX = originalEvent.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - 534 ( doc && doc.clientLeft || body && body.clientLeft || 0); 535 536 event.pageY = originalEvent.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0 ) - 537 ( doc && doc.clientTop || body && body.clientTop || 0); 538 } 539 540 // Add preventDefault method 541 event.preventDefault = function() { 542 event.isDefaultPrevented = returnTrue; 543 544 // Execute preventDefault on the original event object 545 if (originalEvent) { 546 if (originalEvent.preventDefault) { 547 originalEvent.preventDefault(); 548 } else { 549 originalEvent.returnValue = false; // IE 550 } 551 } 552 }; 553 554 // Add stopPropagation 555 event.stopPropagation = function() { 556 event.isPropagationStopped = returnTrue; 557 558 // Execute stopPropagation on the original event object 559 if (originalEvent) { 560 if (originalEvent.stopPropagation) { 561 originalEvent.stopPropagation(); 562 } else { 563 originalEvent.cancelBubble = true; // IE 564 } 565 } 566 }; 567 568 // Add stopImmediatePropagation 569 event.stopImmediatePropagation = function() { 570 event.isImmediatePropagationStopped = returnTrue; 571 event.stopPropagation(); 572 }; 573 574 // Add event delegation states 575 if (!event.isDefaultPrevented) { 576 event.isDefaultPrevented = returnFalse; 577 event.isPropagationStopped = returnFalse; 578 event.isImmediatePropagationStopped = returnFalse; 579 } 580 581 return event; 582 } 583 584 /** 585 * Bind a DOMContentLoaded event across browsers and executes the callback once the page DOM is initialized. 586 * It will also set/check the domLoaded state of the event_utils instance so ready isn't called multiple times. 587 */ 588 function bindOnReady(win, callback, eventUtils) { 589 var doc = win.document, event = {type: 'ready'}; 590 591 if (eventUtils.domLoaded) { 592 callback(event); 593 return; 594 } 595 596 // Gets called when the DOM is ready 597 function readyHandler() { 598 if (!eventUtils.domLoaded) { 599 eventUtils.domLoaded = true; 600 callback(event); 601 } 602 } 603 604 function waitForDomLoaded() { 605 // Check complete or interactive state if there is a body 606 // element on some iframes IE 8 will produce a null body 607 if (doc.readyState === "complete" || (doc.readyState === "interactive" && doc.body)) { 608 removeEvent(doc, "readystatechange", waitForDomLoaded); 609 readyHandler(); 610 } 611 } 612 613 function tryScroll() { 614 try { 615 // If IE is used, use the trick by Diego Perini licensed under MIT by request to the author. 616 // http://javascript.nwbox.com/IEContentLoaded/ 617 doc.documentElement.doScroll("left"); 618 } catch (ex) { 619 setTimeout(tryScroll, 0); 620 return; 621 } 622 623 readyHandler(); 624 } 625 626 // Use W3C method 627 if (doc.addEventListener) { 628 if (doc.readyState === "complete") { 629 readyHandler(); 630 } else { 631 addEvent(win, 'DOMContentLoaded', readyHandler); 632 } 633 } else { 634 // Use IE method 635 addEvent(doc, "readystatechange", waitForDomLoaded); 636 637 // Wait until we can scroll, when we can the DOM is initialized 638 if (doc.documentElement.doScroll && win.self === win.top) { 639 tryScroll(); 640 } 641 } 642 643 // Fallback if any of the above methods should fail for some odd reason 644 addEvent(win, 'load', readyHandler); 645 } 646 647 /** 648 * This class enables you to bind/unbind native events to elements and normalize it's behavior across browsers. 649 */ 650 function EventUtils() { 651 var self = this, events = {}, count, expando, hasFocusIn, hasMouseEnterLeave, mouseEnterLeave; 652 653 expando = eventExpandoPrefix + (+new Date()).toString(32); 654 hasMouseEnterLeave = "onmouseenter" in document.documentElement; 655 hasFocusIn = "onfocusin" in document.documentElement; 656 mouseEnterLeave = {mouseenter: 'mouseover', mouseleave: 'mouseout'}; 657 count = 1; 658 659 // State if the DOMContentLoaded was executed or not 660 self.domLoaded = false; 661 self.events = events; 662 663 /** 664 * Executes all event handler callbacks for a specific event. 665 * 666 * @private 667 * @param {Event} evt Event object. 668 * @param {String} id Expando id value to look for. 669 */ 670 function executeHandlers(evt, id) { 671 var callbackList, i, l, callback, container = events[id]; 672 673 callbackList = container && container[evt.type]; 674 if (callbackList) { 675 for (i = 0, l = callbackList.length; i < l; i++) { 676 callback = callbackList[i]; 677 678 // Check if callback exists might be removed if a unbind is called inside the callback 679 if (callback && callback.func.call(callback.scope, evt) === false) { 680 evt.preventDefault(); 681 } 682 683 // Should we stop propagation to immediate listeners 684 if (evt.isImmediatePropagationStopped()) { 685 return; 686 } 687 } 688 } 689 } 690 691 /** 692 * Binds a callback to an event on the specified target. 693 * 694 * @method bind 695 * @param {Object} target Target node/window or custom object. 696 * @param {String} names Name of the event to bind. 697 * @param {function} callback Callback function to execute when the event occurs. 698 * @param {Object} scope Scope to call the callback function on, defaults to target. 699 * @return {function} Callback function that got bound. 700 */ 701 self.bind = function(target, names, callback, scope) { 702 var id, callbackList, i, name, fakeName, nativeHandler, capture, win = window; 703 704 // Native event handler function patches the event and executes the callbacks for the expando 705 function defaultNativeHandler(evt) { 706 executeHandlers(fix(evt || win.event), id); 707 } 708 709 // Don't bind to text nodes or comments 710 if (!target || target.nodeType === 3 || target.nodeType === 8) { 711 return; 712 } 713 714 // Create or get events id for the target 715 if (!target[expando]) { 716 id = count++; 717 target[expando] = id; 718 events[id] = {}; 719 } else { 720 id = target[expando]; 721 } 722 723 // Setup the specified scope or use the target as a default 724 scope = scope || target; 725 726 // Split names and bind each event, enables you to bind multiple events with one call 727 names = names.split(' '); 728 i = names.length; 729 while (i--) { 730 name = names[i]; 731 nativeHandler = defaultNativeHandler; 732 fakeName = capture = false; 733 734 // Use ready instead of DOMContentLoaded 735 if (name === "DOMContentLoaded") { 736 name = "ready"; 737 } 738 739 // DOM is already ready 740 if (self.domLoaded && name === "ready" && target.readyState == 'complete') { 741 callback.call(scope, fix({type: name})); 742 continue; 743 } 744 745 // Handle mouseenter/mouseleaver 746 if (!hasMouseEnterLeave) { 747 fakeName = mouseEnterLeave[name]; 748 749 if (fakeName) { 750 nativeHandler = function(evt) { 751 var current, related; 752 753 current = evt.currentTarget; 754 related = evt.relatedTarget; 755 756 // Check if related is inside the current target if it's not then the event should 757 // be ignored since it's a mouseover/mouseout inside the element 758 if (related && current.contains) { 759 // Use contains for performance 760 related = current.contains(related); 761 } else { 762 while (related && related !== current) { 763 related = related.parentNode; 764 } 765 } 766 767 // Fire fake event 768 if (!related) { 769 evt = fix(evt || win.event); 770 evt.type = evt.type === 'mouseout' ? 'mouseleave' : 'mouseenter'; 771 evt.target = current; 772 executeHandlers(evt, id); 773 } 774 }; 775 } 776 } 777 778 // Fake bubbeling of focusin/focusout 779 if (!hasFocusIn && (name === "focusin" || name === "focusout")) { 780 capture = true; 781 fakeName = name === "focusin" ? "focus" : "blur"; 782 nativeHandler = function(evt) { 783 evt = fix(evt || win.event); 784 evt.type = evt.type === 'focus' ? 'focusin' : 'focusout'; 785 executeHandlers(evt, id); 786 }; 787 } 788 789 // Setup callback list and bind native event 790 callbackList = events[id][name]; 791 if (!callbackList) { 792 events[id][name] = callbackList = [{func: callback, scope: scope}]; 793 callbackList.fakeName = fakeName; 794 callbackList.capture = capture; 795 796 // Add the nativeHandler to the callback list so that we can later unbind it 797 callbackList.nativeHandler = nativeHandler; 798 799 // Check if the target has native events support 800 801 if (name === "ready") { 802 bindOnReady(target, nativeHandler, self); 803 } else { 804 addEvent(target, fakeName || name, nativeHandler, capture); 805 } 806 } else { 807 if (name === "ready" && self.domLoaded) { 808 callback({type: name}); 809 } else { 810 // If it already has an native handler then just push the callback 811 callbackList.push({func: callback, scope: scope}); 812 } 813 } 814 } 815 816 target = callbackList = 0; // Clean memory for IE 817 818 return callback; 819 }; 820 821 /** 822 * Unbinds the specified event by name, name and callback or all events on the target. 823 * 824 * @method unbind 825 * @param {Object} target Target node/window or custom object. 826 * @param {String} names Optional event name to unbind. 827 * @param {function} callback Optional callback function to unbind. 828 * @return {EventUtils} Event utils instance. 829 */ 830 self.unbind = function(target, names, callback) { 831 var id, callbackList, i, ci, name, eventMap; 832 833 // Don't bind to text nodes or comments 834 if (!target || target.nodeType === 3 || target.nodeType === 8) { 835 return self; 836 } 837 838 // Unbind event or events if the target has the expando 839 id = target[expando]; 840 if (id) { 841 eventMap = events[id]; 842 843 // Specific callback 844 if (names) { 845 names = names.split(' '); 846 i = names.length; 847 while (i--) { 848 name = names[i]; 849 callbackList = eventMap[name]; 850 851 // Unbind the event if it exists in the map 852 if (callbackList) { 853 // Remove specified callback 854 if (callback) { 855 ci = callbackList.length; 856 while (ci--) { 857 if (callbackList[ci].func === callback) { 858 var nativeHandler = callbackList.nativeHandler; 859 var fakeName = callbackList.fakeName, capture = callbackList.capture; 860 861 // Clone callbackList since unbind inside a callback would otherwise break the handlers loop 862 callbackList = callbackList.slice(0, ci).concat(callbackList.slice(ci + 1)); 863 callbackList.nativeHandler = nativeHandler; 864 callbackList.fakeName = fakeName; 865 callbackList.capture = capture; 866 867 eventMap[name] = callbackList; 868 } 869 } 870 } 871 872 // Remove all callbacks if there isn't a specified callback or there is no callbacks left 873 if (!callback || callbackList.length === 0) { 874 delete eventMap[name]; 875 removeEvent(target, callbackList.fakeName || name, callbackList.nativeHandler, callbackList.capture); 876 } 877 } 878 } 879 } else { 880 // All events for a specific element 881 for (name in eventMap) { 882 callbackList = eventMap[name]; 883 removeEvent(target, callbackList.fakeName || name, callbackList.nativeHandler, callbackList.capture); 884 } 885 886 eventMap = {}; 887 } 888 889 // Check if object is empty, if it isn't then we won't remove the expando map 890 for (name in eventMap) { 891 return self; 892 } 893 894 // Delete event object 895 delete events[id]; 896 897 // Remove expando from target 898 try { 899 // IE will fail here since it can't delete properties from window 900 delete target[expando]; 901 } catch (ex) { 902 // IE will set it to null 903 target[expando] = null; 904 } 905 } 906 907 return self; 908 }; 909 910 /** 911 * Fires the specified event on the specified target. 912 * 913 * @method fire 914 * @param {Object} target Target node/window or custom object. 915 * @param {String} name Event name to fire. 916 * @param {Object} args Optional arguments to send to the observers. 917 * @return {EventUtils} Event utils instance. 918 */ 919 self.fire = function(target, name, args) { 920 var id; 921 922 // Don't bind to text nodes or comments 923 if (!target || target.nodeType === 3 || target.nodeType === 8) { 924 return self; 925 } 926 927 // Build event object by patching the args 928 args = fix(null, args); 929 args.type = name; 930 args.target = target; 931 932 do { 933 // Found an expando that means there is listeners to execute 934 id = target[expando]; 935 if (id) { 936 executeHandlers(args, id); 937 } 938 939 // Walk up the DOM 940 target = target.parentNode || target.ownerDocument || target.defaultView || target.parentWindow; 941 } while (target && !args.isPropagationStopped()); 942 943 return self; 944 }; 945 946 /** 947 * Removes all bound event listeners for the specified target. This will also remove any bound 948 * listeners to child nodes within that target. 949 * 950 * @method clean 951 * @param {Object} target Target node/window object. 952 * @return {EventUtils} Event utils instance. 953 */ 954 self.clean = function(target) { 955 var i, children, unbind = self.unbind; 956 957 // Don't bind to text nodes or comments 958 if (!target || target.nodeType === 3 || target.nodeType === 8) { 959 return self; 960 } 961 962 // Unbind any element on the specificed target 963 if (target[expando]) { 964 unbind(target); 965 } 966 967 // Target doesn't have getElementsByTagName it's probably a window object then use it's document to find the children 968 if (!target.getElementsByTagName) { 969 target = target.document; 970 } 971 972 // Remove events from each child element 973 if (target && target.getElementsByTagName) { 974 unbind(target); 975 976 children = target.getElementsByTagName('*'); 977 i = children.length; 978 while (i--) { 979 target = children[i]; 980 981 if (target[expando]) { 982 unbind(target); 983 } 984 } 985 } 986 987 return self; 988 }; 989 990 /** 991 * Destroys the event object. Call this on IE to remove memory leaks. 992 */ 993 self.destroy = function() { 994 events = {}; 995 }; 996 997 // Legacy function for canceling events 998 self.cancel = function(e) { 999 if (e) { 1000 e.preventDefault(); 1001 e.stopImmediatePropagation(); 1002 } 1003 1004 return false; 1005 }; 1006 } 1007 1008 EventUtils.Event = new EventUtils(); 1009 EventUtils.Event.bind(window, 'ready', function() {}); 1010 1011 return EventUtils; 1012 }); 1013 1014 // Included from: js/tinymce/classes/dom/TreeWalker.js 1015 1016 /** 1017 * TreeWalker.js 1018 * 1019 * Copyright, Moxiecode Systems AB 1020 * Released under LGPL License. 1021 * 1022 * License: http://www.tinymce.com/license 1023 * Contributing: http://www.tinymce.com/contributing 1024 */ 1025 1026 /** 1027 * TreeWalker class enables you to walk the DOM in a linear manner. 1028 * 1029 * @class tinymce.dom.TreeWalker 1030 */ 1031 define("tinymce/dom/TreeWalker", [], function() { 1032 return function(start_node, root_node) { 1033 var node = start_node; 1034 1035 function findSibling(node, start_name, sibling_name, shallow) { 1036 var sibling, parent; 1037 1038 if (node) { 1039 // Walk into nodes if it has a start 1040 if (!shallow && node[start_name]) { 1041 return node[start_name]; 1042 } 1043 1044 // Return the sibling if it has one 1045 if (node != root_node) { 1046 sibling = node[sibling_name]; 1047 if (sibling) { 1048 return sibling; 1049 } 1050 1051 // Walk up the parents to look for siblings 1052 for (parent = node.parentNode; parent && parent != root_node; parent = parent.parentNode) { 1053 sibling = parent[sibling_name]; 1054 if (sibling) { 1055 return sibling; 1056 } 1057 } 1058 } 1059 } 1060 } 1061 1062 /** 1063 * Returns the current node. 1064 * 1065 * @method current 1066 * @return {Node} Current node where the walker is. 1067 */ 1068 this.current = function() { 1069 return node; 1070 }; 1071 1072 /** 1073 * Walks to the next node in tree. 1074 * 1075 * @method next 1076 * @return {Node} Current node where the walker is after moving to the next node. 1077 */ 1078 this.next = function(shallow) { 1079 node = findSibling(node, 'firstChild', 'nextSibling', shallow); 1080 return node; 1081 }; 1082 1083 /** 1084 * Walks to the previous node in tree. 1085 * 1086 * @method prev 1087 * @return {Node} Current node where the walker is after moving to the previous node. 1088 */ 1089 this.prev = function(shallow) { 1090 node = findSibling(node, 'lastChild', 'previousSibling', shallow); 1091 return node; 1092 }; 1093 }; 1094 }); 1095 1096 // Included from: js/tinymce/classes/util/Tools.js 1097 1098 /** 1099 * Tools.js 1100 * 1101 * Copyright, Moxiecode Systems AB 1102 * Released under LGPL License. 1103 * 1104 * License: http://www.tinymce.com/license 1105 * Contributing: http://www.tinymce.com/contributing 1106 */ 1107 1108 /** 1109 * This class contains various utlity functions. These are also exposed 1110 * directly on the tinymce namespace. 1111 * 1112 * @class tinymce.util.Tools 1113 */ 1114 define("tinymce/util/Tools", [], function() { 1115 /** 1116 * Removes whitespace from the beginning and end of a string. 1117 * 1118 * @method trim 1119 * @param {String} s String to remove whitespace from. 1120 * @return {String} New string with removed whitespace. 1121 */ 1122 var whiteSpaceRegExp = /^\s*|\s*$/g; 1123 1124 function trim(str) { 1125 return (str === null || str === undefined) ? '' : ("" + str).replace(whiteSpaceRegExp, ''); 1126 } 1127 1128 /** 1129 * Returns true/false if the object is an array or not. 1130 * 1131 * @method isArray 1132 * @param {Object} obj Object to check. 1133 * @return {boolean} true/false state if the object is an array or not. 1134 */ 1135 var isArray = Array.isArray || function(obj) { 1136 return Object.prototype.toString.call(obj) === "[object Array]"; 1137 }; 1138 1139 /** 1140 * Checks if a object is of a specific type for example an array. 1141 * 1142 * @method is 1143 * @param {Object} o Object to check type of. 1144 * @param {string} t Optional type to check for. 1145 * @return {Boolean} true/false if the object is of the specified type. 1146 */ 1147 function is(o, t) { 1148 if (!t) { 1149 return o !== undefined; 1150 } 1151 1152 if (t == 'array' && isArray(o)) { 1153 return true; 1154 } 1155 1156 return typeof(o) == t; 1157 } 1158 1159 /** 1160 * Converts the specified object into a real JavaScript array. 1161 * 1162 * @method toArray 1163 * @param {Object} obj Object to convert into array. 1164 * @return {Array} Array object based in input. 1165 */ 1166 function toArray(obj) { 1167 var array = [], i, l; 1168 1169 for (i = 0, l = obj.length; i < l; i++) { 1170 array[i] = obj[i]; 1171 } 1172 1173 return array; 1174 } 1175 1176 /** 1177 * Makes a name/object map out of an array with names. 1178 * 1179 * @method makeMap 1180 * @param {Array/String} items Items to make map out of. 1181 * @param {String} delim Optional delimiter to split string by. 1182 * @param {Object} map Optional map to add items to. 1183 * @return {Object} Name/value map of items. 1184 */ 1185 function makeMap(items, delim, map) { 1186 var i; 1187 1188 items = items || []; 1189 delim = delim || ','; 1190 1191 if (typeof(items) == "string") { 1192 items = items.split(delim); 1193 } 1194 1195 map = map || {}; 1196 1197 i = items.length; 1198 while (i--) { 1199 map[items[i]] = {}; 1200 } 1201 1202 return map; 1203 } 1204 1205 /** 1206 * Performs an iteration of all items in a collection such as an object or array. This method will execure the 1207 * callback function for each item in the collection, if the callback returns false the iteration will terminate. 1208 * The callback has the following format: cb(value, key_or_index). 1209 * 1210 * @method each 1211 * @param {Object} o Collection to iterate. 1212 * @param {function} cb Callback function to execute for each item. 1213 * @param {Object} s Optional scope to execute the callback in. 1214 * @example 1215 * // Iterate an array 1216 * tinymce.each([1,2,3], function(v, i) { 1217 * console.debug("Value: " + v + ", Index: " + i); 1218 * }); 1219 * 1220 * // Iterate an object 1221 * tinymce.each({a: 1, b: 2, c: 3], function(v, k) { 1222 * console.debug("Value: " + v + ", Key: " + k); 1223 * }); 1224 */ 1225 function each(o, cb, s) { 1226 var n, l; 1227 1228 if (!o) { 1229 return 0; 1230 } 1231 1232 s = s || o; 1233 1234 if (o.length !== undefined) { 1235 // Indexed arrays, needed for Safari 1236 for (n = 0, l = o.length; n < l; n++) { 1237 if (cb.call(s, o[n], n, o) === false) { 1238 return 0; 1239 } 1240 } 1241 } else { 1242 // Hashtables 1243 for (n in o) { 1244 if (o.hasOwnProperty(n)) { 1245 if (cb.call(s, o[n], n, o) === false) { 1246 return 0; 1247 } 1248 } 1249 } 1250 } 1251 1252 return 1; 1253 } 1254 1255 /** 1256 * Creates a new array by the return value of each iteration function call. This enables you to convert 1257 * one array list into another. 1258 * 1259 * @method map 1260 * @param {Array} a Array of items to iterate. 1261 * @param {function} f Function to call for each item. It's return value will be the new value. 1262 * @return {Array} Array with new values based on function return values. 1263 */ 1264 function map(a, f) { 1265 var o = []; 1266 1267 each(a, function(v) { 1268 o.push(f(v)); 1269 }); 1270 1271 return o; 1272 } 1273 1274 /** 1275 * Filters out items from the input array by calling the specified function for each item. 1276 * If the function returns false the item will be excluded if it returns true it will be included. 1277 * 1278 * @method grep 1279 * @param {Array} a Array of items to loop though. 1280 * @param {function} f Function to call for each item. Include/exclude depends on it's return value. 1281 * @return {Array} New array with values imported and filtered based in input. 1282 * @example 1283 * // Filter out some items, this will return an array with 4 and 5 1284 * var items = tinymce.grep([1,2,3,4,5], function(v) {return v > 3;}); 1285 */ 1286 function grep(a, f) { 1287 var o = []; 1288 1289 each(a, function(v) { 1290 if (!f || f(v)) { 1291 o.push(v); 1292 } 1293 }); 1294 1295 return o; 1296 } 1297 1298 /** 1299 * Creates a class, subclass or static singleton. 1300 * More details on this method can be found in the Wiki. 1301 * 1302 * @method create 1303 * @param {String} s Class name, inheritage and prefix. 1304 * @param {Object} p Collection of methods to add to the class. 1305 * @param {Object} root Optional root object defaults to the global window object. 1306 * @example 1307 * // Creates a basic class 1308 * tinymce.create('tinymce.somepackage.SomeClass', { 1309 * SomeClass: function() { 1310 * // Class constructor 1311 * }, 1312 * 1313 * method: function() { 1314 * // Some method 1315 * } 1316 * }); 1317 * 1318 * // Creates a basic subclass class 1319 * tinymce.create('tinymce.somepackage.SomeSubClass:tinymce.somepackage.SomeClass', { 1320 * SomeSubClass: function() { 1321 * // Class constructor 1322 * this.parent(); // Call parent constructor 1323 * }, 1324 * 1325 * method: function() { 1326 * // Some method 1327 * this.parent(); // Call parent method 1328 * }, 1329 * 1330 * 'static': { 1331 * staticMethod: function() { 1332 * // Static method 1333 * } 1334 * } 1335 * }); 1336 * 1337 * // Creates a singleton/static class 1338 * tinymce.create('static tinymce.somepackage.SomeSingletonClass', { 1339 * method: function() { 1340 * // Some method 1341 * } 1342 * }); 1343 */ 1344 function create(s, p, root) { 1345 var self = this, sp, ns, cn, scn, c, de = 0; 1346 1347 // Parse : <prefix> <class>:<super class> 1348 s = /^((static) )?([\w.]+)(:([\w.]+))?/.exec(s); 1349 cn = s[3].match(/(^|\.)(\w+)$/i)[2]; // Class name 1350 1351 // Create namespace for new class 1352 ns = self.createNS(s[3].replace(/\.\w+$/, ''), root); 1353 1354 // Class already exists 1355 if (ns[cn]) { 1356 return; 1357 } 1358 1359 // Make pure static class 1360 if (s[2] == 'static') { 1361 ns[cn] = p; 1362 1363 if (this.onCreate) { 1364 this.onCreate(s[2], s[3], ns[cn]); 1365 } 1366 1367 return; 1368 } 1369 1370 // Create default constructor 1371 if (!p[cn]) { 1372 p[cn] = function() {}; 1373 de = 1; 1374 } 1375 1376 // Add constructor and methods 1377 ns[cn] = p[cn]; 1378 self.extend(ns[cn].prototype, p); 1379 1380 // Extend 1381 if (s[5]) { 1382 sp = self.resolve(s[5]).prototype; 1383 scn = s[5].match(/\.(\w+)$/i)[1]; // Class name 1384 1385 // Extend constructor 1386 c = ns[cn]; 1387 if (de) { 1388 // Add passthrough constructor 1389 ns[cn] = function() { 1390 return sp[scn].apply(this, arguments); 1391 }; 1392 } else { 1393 // Add inherit constructor 1394 ns[cn] = function() { 1395 this.parent = sp[scn]; 1396 return c.apply(this, arguments); 1397 }; 1398 } 1399 ns[cn].prototype[cn] = ns[cn]; 1400 1401 // Add super methods 1402 self.each(sp, function(f, n) { 1403 ns[cn].prototype[n] = sp[n]; 1404 }); 1405 1406 // Add overridden methods 1407 self.each(p, function(f, n) { 1408 // Extend methods if needed 1409 if (sp[n]) { 1410 ns[cn].prototype[n] = function() { 1411 this.parent = sp[n]; 1412 return f.apply(this, arguments); 1413 }; 1414 } else { 1415 if (n != cn) { 1416 ns[cn].prototype[n] = f; 1417 } 1418 } 1419 }); 1420 } 1421 1422 // Add static methods 1423 /*jshint sub:true*/ 1424 self.each(p['static'], function(f, n) { 1425 ns[cn][n] = f; 1426 }); 1427 } 1428 1429 /** 1430 * Returns the index of a value in an array, this method will return -1 if the item wasn't found. 1431 * 1432 * @method inArray 1433 * @param {Array} a Array/Object to search for value in. 1434 * @param {Object} v Value to check for inside the array. 1435 * @return {Number/String} Index of item inside the array inside an object. Or -1 if it wasn't found. 1436 * @example 1437 * // Get index of value in array this will alert 1 since 2 is at that index 1438 * alert(tinymce.inArray([1,2,3], 2)); 1439 */ 1440 function inArray(a, v) { 1441 var i, l; 1442 1443 if (a) { 1444 for (i = 0, l = a.length; i < l; i++) { 1445 if (a[i] === v) { 1446 return i; 1447 } 1448 } 1449 } 1450 1451 return -1; 1452 } 1453 1454 function extend(obj, ext) { 1455 var i, l, name, args = arguments, value; 1456 1457 for (i = 1, l = args.length; i < l; i++) { 1458 ext = args[i]; 1459 for (name in ext) { 1460 if (ext.hasOwnProperty(name)) { 1461 value = ext[name]; 1462 1463 if (value !== undefined) { 1464 obj[name] = value; 1465 } 1466 } 1467 } 1468 } 1469 1470 return obj; 1471 } 1472 1473 /** 1474 * Executed the specified function for each item in a object tree. 1475 * 1476 * @method walk 1477 * @param {Object} o Object tree to walk though. 1478 * @param {function} f Function to call for each item. 1479 * @param {String} n Optional name of collection inside the objects to walk for example childNodes. 1480 * @param {String} s Optional scope to execute the function in. 1481 */ 1482 function walk(o, f, n, s) { 1483 s = s || this; 1484 1485 if (o) { 1486 if (n) { 1487 o = o[n]; 1488 } 1489 1490 each(o, function(o, i) { 1491 if (f.call(s, o, i, n) === false) { 1492 return false; 1493 } 1494 1495 walk(o, f, n, s); 1496 }); 1497 } 1498 } 1499 1500 /** 1501 * Creates a namespace on a specific object. 1502 * 1503 * @method createNS 1504 * @param {String} n Namespace to create for example a.b.c.d. 1505 * @param {Object} o Optional object to add namespace to, defaults to window. 1506 * @return {Object} New namespace object the last item in path. 1507 * @example 1508 * // Create some namespace 1509 * tinymce.createNS('tinymce.somepackage.subpackage'); 1510 * 1511 * // Add a singleton 1512 * var tinymce.somepackage.subpackage.SomeSingleton = { 1513 * method: function() { 1514 * // Some method 1515 * } 1516 * }; 1517 */ 1518 function createNS(n, o) { 1519 var i, v; 1520 1521 o = o || window; 1522 1523 n = n.split('.'); 1524 for (i = 0; i < n.length; i++) { 1525 v = n[i]; 1526 1527 if (!o[v]) { 1528 o[v] = {}; 1529 } 1530 1531 o = o[v]; 1532 } 1533 1534 return o; 1535 } 1536 1537 /** 1538 * Resolves a string and returns the object from a specific structure. 1539 * 1540 * @method resolve 1541 * @param {String} n Path to resolve for example a.b.c.d. 1542 * @param {Object} o Optional object to search though, defaults to window. 1543 * @return {Object} Last object in path or null if it couldn't be resolved. 1544 * @example 1545 * // Resolve a path into an object reference 1546 * var obj = tinymce.resolve('a.b.c.d'); 1547 */ 1548 function resolve(n, o) { 1549 var i, l; 1550 1551 o = o || window; 1552 1553 n = n.split('.'); 1554 for (i = 0, l = n.length; i < l; i++) { 1555 o = o[n[i]]; 1556 1557 if (!o) { 1558 break; 1559 } 1560 } 1561 1562 return o; 1563 } 1564 1565 /** 1566 * Splits a string but removes the whitespace before and after each value. 1567 * 1568 * @method explode 1569 * @param {string} s String to split. 1570 * @param {string} d Delimiter to split by. 1571 * @example 1572 * // Split a string into an array with a,b,c 1573 * var arr = tinymce.explode('a, b, c'); 1574 */ 1575 function explode(s, d) { 1576 if (!s || is(s, 'array')) { 1577 return s; 1578 } 1579 1580 return map(s.split(d || ','), trim); 1581 } 1582 1583 return { 1584 trim: trim, 1585 isArray: isArray, 1586 is: is, 1587 toArray: toArray, 1588 makeMap: makeMap, 1589 each: each, 1590 map: map, 1591 grep: grep, 1592 inArray: inArray, 1593 extend: extend, 1594 create: create, 1595 walk: walk, 1596 createNS: createNS, 1597 resolve: resolve, 1598 explode: explode 1599 }; 1600 }); 1601 1602 // Included from: js/tinymce/classes/dom/Range.js 1603 1604 /** 1605 * Range.js 1606 * 1607 * Copyright, Moxiecode Systems AB 1608 * Released under LGPL License. 1609 * 1610 * License: http://www.tinymce.com/license 1611 * Contributing: http://www.tinymce.com/contributing 1612 */ 1613 1614 define("tinymce/dom/Range", [ 1615 "tinymce/util/Tools" 1616 ], function(Tools) { 1617 // Range constructor 1618 function Range(dom) { 1619 var self = this, 1620 doc = dom.doc, 1621 EXTRACT = 0, 1622 CLONE = 1, 1623 DELETE = 2, 1624 TRUE = true, 1625 FALSE = false, 1626 START_OFFSET = 'startOffset', 1627 START_CONTAINER = 'startContainer', 1628 END_CONTAINER = 'endContainer', 1629 END_OFFSET = 'endOffset', 1630 extend = Tools.extend, 1631 nodeIndex = dom.nodeIndex; 1632 1633 function createDocumentFragment() { 1634 return doc.createDocumentFragment(); 1635 } 1636 1637 function setStart(n, o) { 1638 _setEndPoint(TRUE, n, o); 1639 } 1640 1641 function setEnd(n, o) { 1642 _setEndPoint(FALSE, n, o); 1643 } 1644 1645 function setStartBefore(n) { 1646 setStart(n.parentNode, nodeIndex(n)); 1647 } 1648 1649 function setStartAfter(n) { 1650 setStart(n.parentNode, nodeIndex(n) + 1); 1651 } 1652 1653 function setEndBefore(n) { 1654 setEnd(n.parentNode, nodeIndex(n)); 1655 } 1656 1657 function setEndAfter(n) { 1658 setEnd(n.parentNode, nodeIndex(n) + 1); 1659 } 1660 1661 function collapse(ts) { 1662 if (ts) { 1663 self[END_CONTAINER] = self[START_CONTAINER]; 1664 self[END_OFFSET] = self[START_OFFSET]; 1665 } else { 1666 self[START_CONTAINER] = self[END_CONTAINER]; 1667 self[START_OFFSET] = self[END_OFFSET]; 1668 } 1669 1670 self.collapsed = TRUE; 1671 } 1672 1673 function selectNode(n) { 1674 setStartBefore(n); 1675 setEndAfter(n); 1676 } 1677 1678 function selectNodeContents(n) { 1679 setStart(n, 0); 1680 setEnd(n, n.nodeType === 1 ? n.childNodes.length : n.nodeValue.length); 1681 } 1682 1683 function compareBoundaryPoints(h, r) { 1684 var sc = self[START_CONTAINER], so = self[START_OFFSET], ec = self[END_CONTAINER], eo = self[END_OFFSET], 1685 rsc = r.startContainer, rso = r.startOffset, rec = r.endContainer, reo = r.endOffset; 1686 1687 // Check START_TO_START 1688 if (h === 0) { 1689 return _compareBoundaryPoints(sc, so, rsc, rso); 1690 } 1691 1692 // Check START_TO_END 1693 if (h === 1) { 1694 return _compareBoundaryPoints(ec, eo, rsc, rso); 1695 } 1696 1697 // Check END_TO_END 1698 if (h === 2) { 1699 return _compareBoundaryPoints(ec, eo, rec, reo); 1700 } 1701 1702 // Check END_TO_START 1703 if (h === 3) { 1704 return _compareBoundaryPoints(sc, so, rec, reo); 1705 } 1706 } 1707 1708 function deleteContents() { 1709 _traverse(DELETE); 1710 } 1711 1712 function extractContents() { 1713 return _traverse(EXTRACT); 1714 } 1715 1716 function cloneContents() { 1717 return _traverse(CLONE); 1718 } 1719 1720 function insertNode(n) { 1721 var startContainer = this[START_CONTAINER], 1722 startOffset = this[START_OFFSET], nn, o; 1723 1724 // Node is TEXT_NODE or CDATA 1725 if ((startContainer.nodeType === 3 || startContainer.nodeType === 4) && startContainer.nodeValue) { 1726 if (!startOffset) { 1727 // At the start of text 1728 startContainer.parentNode.insertBefore(n, startContainer); 1729 } else if (startOffset >= startContainer.nodeValue.length) { 1730 // At the end of text 1731 dom.insertAfter(n, startContainer); 1732 } else { 1733 // Middle, need to split 1734 nn = startContainer.splitText(startOffset); 1735 startContainer.parentNode.insertBefore(n, nn); 1736 } 1737 } else { 1738 // Insert element node 1739 if (startContainer.childNodes.length > 0) { 1740 o = startContainer.childNodes[startOffset]; 1741 } 1742 1743 if (o) { 1744 startContainer.insertBefore(n, o); 1745 } else { 1746 if (startContainer.nodeType == 3) { 1747 dom.insertAfter(n, startContainer); 1748 } else { 1749 startContainer.appendChild(n); 1750 } 1751 } 1752 } 1753 } 1754 1755 function surroundContents(n) { 1756 var f = self.extractContents(); 1757 1758 self.insertNode(n); 1759 n.appendChild(f); 1760 self.selectNode(n); 1761 } 1762 1763 function cloneRange() { 1764 return extend(new Range(dom), { 1765 startContainer: self[START_CONTAINER], 1766 startOffset: self[START_OFFSET], 1767 endContainer: self[END_CONTAINER], 1768 endOffset: self[END_OFFSET], 1769 collapsed: self.collapsed, 1770 commonAncestorContainer: self.commonAncestorContainer 1771 }); 1772 } 1773 1774 // Private methods 1775 1776 function _getSelectedNode(container, offset) { 1777 var child; 1778 1779 if (container.nodeType == 3 /* TEXT_NODE */) { 1780 return container; 1781 } 1782 1783 if (offset < 0) { 1784 return container; 1785 } 1786 1787 child = container.firstChild; 1788 while (child && offset > 0) { 1789 --offset; 1790 child = child.nextSibling; 1791 } 1792 1793 if (child) { 1794 return child; 1795 } 1796 1797 return container; 1798 } 1799 1800 function _isCollapsed() { 1801 return (self[START_CONTAINER] == self[END_CONTAINER] && self[START_OFFSET] == self[END_OFFSET]); 1802 } 1803 1804 function _compareBoundaryPoints(containerA, offsetA, containerB, offsetB) { 1805 var c, offsetC, n, cmnRoot, childA, childB; 1806 1807 // In the first case the boundary-points have the same container. A is before B 1808 // if its offset is less than the offset of B, A is equal to B if its offset is 1809 // equal to the offset of B, and A is after B if its offset is greater than the 1810 // offset of B. 1811 if (containerA == containerB) { 1812 if (offsetA == offsetB) { 1813 return 0; // equal 1814 } 1815 1816 if (offsetA < offsetB) { 1817 return -1; // before 1818 } 1819 1820 return 1; // after 1821 } 1822 1823 // In the second case a child node C of the container of A is an ancestor 1824 // container of B. In this case, A is before B if the offset of A is less than or 1825 // equal to the index of the child node C and A is after B otherwise. 1826 c = containerB; 1827 while (c && c.parentNode != containerA) { 1828 c = c.parentNode; 1829 } 1830 1831 if (c) { 1832 offsetC = 0; 1833 n = containerA.firstChild; 1834 1835 while (n != c && offsetC < offsetA) { 1836 offsetC++; 1837 n = n.nextSibling; 1838 } 1839 1840 if (offsetA <= offsetC) { 1841 return -1; // before 1842 } 1843 1844 return 1; // after 1845 } 1846 1847 // In the third case a child node C of the container of B is an ancestor container 1848 // of A. In this case, A is before B if the index of the child node C is less than 1849 // the offset of B and A is after B otherwise. 1850 c = containerA; 1851 while (c && c.parentNode != containerB) { 1852 c = c.parentNode; 1853 } 1854 1855 if (c) { 1856 offsetC = 0; 1857 n = containerB.firstChild; 1858 1859 while (n != c && offsetC < offsetB) { 1860 offsetC++; 1861 n = n.nextSibling; 1862 } 1863 1864 if (offsetC < offsetB) { 1865 return -1; // before 1866 } 1867 1868 return 1; // after 1869 } 1870 1871 // In the fourth case, none of three other cases hold: the containers of A and B 1872 // are siblings or descendants of sibling nodes. In this case, A is before B if 1873 // the container of A is before the container of B in a pre-order traversal of the 1874 // Ranges' context tree and A is after B otherwise. 1875 cmnRoot = dom.findCommonAncestor(containerA, containerB); 1876 childA = containerA; 1877 1878 while (childA && childA.parentNode != cmnRoot) { 1879 childA = childA.parentNode; 1880 } 1881 1882 if (!childA) { 1883 childA = cmnRoot; 1884 } 1885 1886 childB = containerB; 1887 while (childB && childB.parentNode != cmnRoot) { 1888 childB = childB.parentNode; 1889 } 1890 1891 if (!childB) { 1892 childB = cmnRoot; 1893 } 1894 1895 if (childA == childB) { 1896 return 0; // equal 1897 } 1898 1899 n = cmnRoot.firstChild; 1900 while (n) { 1901 if (n == childA) { 1902 return -1; // before 1903 } 1904 1905 if (n == childB) { 1906 return 1; // after 1907 } 1908 1909 n = n.nextSibling; 1910 } 1911 } 1912 1913 function _setEndPoint(st, n, o) { 1914 var ec, sc; 1915 1916 if (st) { 1917 self[START_CONTAINER] = n; 1918 self[START_OFFSET] = o; 1919 } else { 1920 self[END_CONTAINER] = n; 1921 self[END_OFFSET] = o; 1922 } 1923 1924 // If one boundary-point of a Range is set to have a root container 1925 // other than the current one for the Range, the Range is collapsed to 1926 // the new position. This enforces the restriction that both boundary- 1927 // points of a Range must have the same root container. 1928 ec = self[END_CONTAINER]; 1929 while (ec.parentNode) { 1930 ec = ec.parentNode; 1931 } 1932 1933 sc = self[START_CONTAINER]; 1934 while (sc.parentNode) { 1935 sc = sc.parentNode; 1936 } 1937 1938 if (sc == ec) { 1939 // The start position of a Range is guaranteed to never be after the 1940 // end position. To enforce this restriction, if the start is set to 1941 // be at a position after the end, the Range is collapsed to that 1942 // position. 1943 if (_compareBoundaryPoints(self[START_CONTAINER], self[START_OFFSET], self[END_CONTAINER], self[END_OFFSET]) > 0) { 1944 self.collapse(st); 1945 } 1946 } else { 1947 self.collapse(st); 1948 } 1949 1950 self.collapsed = _isCollapsed(); 1951 self.commonAncestorContainer = dom.findCommonAncestor(self[START_CONTAINER], self[END_CONTAINER]); 1952 } 1953 1954 function _traverse(how) { 1955 var c, endContainerDepth = 0, startContainerDepth = 0, p, depthDiff, startNode, endNode, sp, ep; 1956 1957 if (self[START_CONTAINER] == self[END_CONTAINER]) { 1958 return _traverseSameContainer(how); 1959 } 1960 1961 for (c = self[END_CONTAINER], p = c.parentNode; p; c = p, p = p.parentNode) { 1962 if (p == self[START_CONTAINER]) { 1963 return _traverseCommonStartContainer(c, how); 1964 } 1965 1966 ++endContainerDepth; 1967 } 1968 1969 for (c = self[START_CONTAINER], p = c.parentNode; p; c = p, p = p.parentNode) { 1970 if (p == self[END_CONTAINER]) { 1971 return _traverseCommonEndContainer(c, how); 1972 } 1973 1974 ++startContainerDepth; 1975 } 1976 1977 depthDiff = startContainerDepth - endContainerDepth; 1978 1979 startNode = self[START_CONTAINER]; 1980 while (depthDiff > 0) { 1981 startNode = startNode.parentNode; 1982 depthDiff--; 1983 } 1984 1985 endNode = self[END_CONTAINER]; 1986 while (depthDiff < 0) { 1987 endNode = endNode.parentNode; 1988 depthDiff++; 1989 } 1990 1991 // ascend the ancestor hierarchy until we have a common parent. 1992 for (sp = startNode.parentNode, ep = endNode.parentNode; sp != ep; sp = sp.parentNode, ep = ep.parentNode) { 1993 startNode = sp; 1994 endNode = ep; 1995 } 1996 1997 return _traverseCommonAncestors(startNode, endNode, how); 1998 } 1999 2000 function _traverseSameContainer(how) { 2001 var frag, s, sub, n, cnt, sibling, xferNode, start, len; 2002 2003 if (how != DELETE) { 2004 frag = createDocumentFragment(); 2005 } 2006 2007 // If selection is empty, just return the fragment 2008 if (self[START_OFFSET] == self[END_OFFSET]) { 2009 return frag; 2010 } 2011 2012 // Text node needs special case handling 2013 if (self[START_CONTAINER].nodeType == 3 /* TEXT_NODE */) { 2014 // get the substring 2015 s = self[START_CONTAINER].nodeValue; 2016 sub = s.substring(self[START_OFFSET], self[END_OFFSET]); 2017 2018 // set the original text node to its new value 2019 if (how != CLONE) { 2020 n = self[START_CONTAINER]; 2021 start = self[START_OFFSET]; 2022 len = self[END_OFFSET] - self[START_OFFSET]; 2023 2024 if (start === 0 && len >= n.nodeValue.length - 1) { 2025 n.parentNode.removeChild(n); 2026 } else { 2027 n.deleteData(start, len); 2028 } 2029 2030 // Nothing is partially selected, so collapse to start point 2031 self.collapse(TRUE); 2032 } 2033 2034 if (how == DELETE) { 2035 return; 2036 } 2037 2038 if (sub.length > 0) { 2039 frag.appendChild(doc.createTextNode(sub)); 2040 } 2041 2042 return frag; 2043 } 2044 2045 // Copy nodes between the start/end offsets. 2046 n = _getSelectedNode(self[START_CONTAINER], self[START_OFFSET]); 2047 cnt = self[END_OFFSET] - self[START_OFFSET]; 2048 2049 while (n && cnt > 0) { 2050 sibling = n.nextSibling; 2051 xferNode = _traverseFullySelected(n, how); 2052 2053 if (frag) { 2054 frag.appendChild(xferNode); 2055 } 2056 2057 --cnt; 2058 n = sibling; 2059 } 2060 2061 // Nothing is partially selected, so collapse to start point 2062 if (how != CLONE) { 2063 self.collapse(TRUE); 2064 } 2065 2066 return frag; 2067 } 2068 2069 function _traverseCommonStartContainer(endAncestor, how) { 2070 var frag, n, endIdx, cnt, sibling, xferNode; 2071 2072 if (how != DELETE) { 2073 frag = createDocumentFragment(); 2074 } 2075 2076 n = _traverseRightBoundary(endAncestor, how); 2077 2078 if (frag) { 2079 frag.appendChild(n); 2080 } 2081 2082 endIdx = nodeIndex(endAncestor); 2083 cnt = endIdx - self[START_OFFSET]; 2084 2085 if (cnt <= 0) { 2086 // Collapse to just before the endAncestor, which 2087 // is partially selected. 2088 if (how != CLONE) { 2089 self.setEndBefore(endAncestor); 2090 self.collapse(FALSE); 2091 } 2092 2093 return frag; 2094 } 2095 2096 n = endAncestor.previousSibling; 2097 while (cnt > 0) { 2098 sibling = n.previousSibling; 2099 xferNode = _traverseFullySelected(n, how); 2100 2101 if (frag) { 2102 frag.insertBefore(xferNode, frag.firstChild); 2103 } 2104 2105 --cnt; 2106 n = sibling; 2107 } 2108 2109 // Collapse to just before the endAncestor, which 2110 // is partially selected. 2111 if (how != CLONE) { 2112 self.setEndBefore(endAncestor); 2113 self.collapse(FALSE); 2114 } 2115 2116 return frag; 2117 } 2118 2119 function _traverseCommonEndContainer(startAncestor, how) { 2120 var frag, startIdx, n, cnt, sibling, xferNode; 2121 2122 if (how != DELETE) { 2123 frag = createDocumentFragment(); 2124 } 2125 2126 n = _traverseLeftBoundary(startAncestor, how); 2127 if (frag) { 2128 frag.appendChild(n); 2129 } 2130 2131 startIdx = nodeIndex(startAncestor); 2132 ++startIdx; // Because we already traversed it 2133 2134 cnt = self[END_OFFSET] - startIdx; 2135 n = startAncestor.nextSibling; 2136 while (n && cnt > 0) { 2137 sibling = n.nextSibling; 2138 xferNode = _traverseFullySelected(n, how); 2139 2140 if (frag) { 2141 frag.appendChild(xferNode); 2142 } 2143 2144 --cnt; 2145 n = sibling; 2146 } 2147 2148 if (how != CLONE) { 2149 self.setStartAfter(startAncestor); 2150 self.collapse(TRUE); 2151 } 2152 2153 return frag; 2154 } 2155 2156 function _traverseCommonAncestors(startAncestor, endAncestor, how) { 2157 var n, frag, startOffset, endOffset, cnt, sibling, nextSibling; 2158 2159 if (how != DELETE) { 2160 frag = createDocumentFragment(); 2161 } 2162 2163 n = _traverseLeftBoundary(startAncestor, how); 2164 if (frag) { 2165 frag.appendChild(n); 2166 } 2167 2168 startOffset = nodeIndex(startAncestor); 2169 endOffset = nodeIndex(endAncestor); 2170 ++startOffset; 2171 2172 cnt = endOffset - startOffset; 2173 sibling = startAncestor.nextSibling; 2174 2175 while (cnt > 0) { 2176 nextSibling = sibling.nextSibling; 2177 n = _traverseFullySelected(sibling, how); 2178 2179 if (frag) { 2180 frag.appendChild(n); 2181 } 2182 2183 sibling = nextSibling; 2184 --cnt; 2185 } 2186 2187 n = _traverseRightBoundary(endAncestor, how); 2188 2189 if (frag) { 2190 frag.appendChild(n); 2191 } 2192 2193 if (how != CLONE) { 2194 self.setStartAfter(startAncestor); 2195 self.collapse(TRUE); 2196 } 2197 2198 return frag; 2199 } 2200 2201 function _traverseRightBoundary(root, how) { 2202 var next = _getSelectedNode(self[END_CONTAINER], self[END_OFFSET] - 1), parent, clonedParent; 2203 var prevSibling, clonedChild, clonedGrandParent, isFullySelected = next != self[END_CONTAINER]; 2204 2205 if (next == root) { 2206 return _traverseNode(next, isFullySelected, FALSE, how); 2207 } 2208 2209 parent = next.parentNode; 2210 clonedParent = _traverseNode(parent, FALSE, FALSE, how); 2211 2212 while (parent) { 2213 while (next) { 2214 prevSibling = next.previousSibling; 2215 clonedChild = _traverseNode(next, isFullySelected, FALSE, how); 2216 2217 if (how != DELETE) { 2218 clonedParent.insertBefore(clonedChild, clonedParent.firstChild); 2219 } 2220 2221 isFullySelected = TRUE; 2222 next = prevSibling; 2223 } 2224 2225 if (parent == root) { 2226 return clonedParent; 2227 } 2228 2229 next = parent.previousSibling; 2230 parent = parent.parentNode; 2231 2232 clonedGrandParent = _traverseNode(parent, FALSE, FALSE, how); 2233 2234 if (how != DELETE) { 2235 clonedGrandParent.appendChild(clonedParent); 2236 } 2237 2238 clonedParent = clonedGrandParent; 2239 } 2240 } 2241 2242 function _traverseLeftBoundary(root, how) { 2243 var next = _getSelectedNode(self[START_CONTAINER], self[START_OFFSET]), isFullySelected = next != self[START_CONTAINER]; 2244 var parent, clonedParent, nextSibling, clonedChild, clonedGrandParent; 2245 2246 if (next == root) { 2247 return _traverseNode(next, isFullySelected, TRUE, how); 2248 } 2249 2250 parent = next.parentNode; 2251 clonedParent = _traverseNode(parent, FALSE, TRUE, how); 2252 2253 while (parent) { 2254 while (next) { 2255 nextSibling = next.nextSibling; 2256 clonedChild = _traverseNode(next, isFullySelected, TRUE, how); 2257 2258 if (how != DELETE) { 2259 clonedParent.appendChild(clonedChild); 2260 } 2261 2262 isFullySelected = TRUE; 2263 next = nextSibling; 2264 } 2265 2266 if (parent == root) { 2267 return clonedParent; 2268 } 2269 2270 next = parent.nextSibling; 2271 parent = parent.parentNode; 2272 2273 clonedGrandParent = _traverseNode(parent, FALSE, TRUE, how); 2274 2275 if (how != DELETE) { 2276 clonedGrandParent.appendChild(clonedParent); 2277 } 2278 2279 clonedParent = clonedGrandParent; 2280 } 2281 } 2282 2283 function _traverseNode(n, isFullySelected, isLeft, how) { 2284 var txtValue, newNodeValue, oldNodeValue, offset, newNode; 2285 2286 if (isFullySelected) { 2287 return _traverseFullySelected(n, how); 2288 } 2289 2290 if (n.nodeType == 3 /* TEXT_NODE */) { 2291 txtValue = n.nodeValue; 2292 2293 if (isLeft) { 2294 offset = self[START_OFFSET]; 2295 newNodeValue = txtValue.substring(offset); 2296 oldNodeValue = txtValue.substring(0, offset); 2297 } else { 2298 offset = self[END_OFFSET]; 2299 newNodeValue = txtValue.substring(0, offset); 2300 oldNodeValue = txtValue.substring(offset); 2301 } 2302 2303 if (how != CLONE) { 2304 n.nodeValue = oldNodeValue; 2305 } 2306 2307 if (how == DELETE) { 2308 return; 2309 } 2310 2311 newNode = dom.clone(n, FALSE); 2312 newNode.nodeValue = newNodeValue; 2313 2314 return newNode; 2315 } 2316 2317 if (how == DELETE) { 2318 return; 2319 } 2320 2321 return dom.clone(n, FALSE); 2322 } 2323 2324 function _traverseFullySelected(n, how) { 2325 if (how != DELETE) { 2326 return how == CLONE ? dom.clone(n, TRUE) : n; 2327 } 2328 2329 n.parentNode.removeChild(n); 2330 } 2331 2332 function toStringIE() { 2333 return dom.create('body', null, cloneContents()).outerText; 2334 } 2335 2336 extend(self, { 2337 // Inital states 2338 startContainer: doc, 2339 startOffset: 0, 2340 endContainer: doc, 2341 endOffset: 0, 2342 collapsed: TRUE, 2343 commonAncestorContainer: doc, 2344 2345 // Range constants 2346 START_TO_START: 0, 2347 START_TO_END: 1, 2348 END_TO_END: 2, 2349 END_TO_START: 3, 2350 2351 // Public methods 2352 setStart: setStart, 2353 setEnd: setEnd, 2354 setStartBefore: setStartBefore, 2355 setStartAfter: setStartAfter, 2356 setEndBefore: setEndBefore, 2357 setEndAfter: setEndAfter, 2358 collapse: collapse, 2359 selectNode: selectNode, 2360 selectNodeContents: selectNodeContents, 2361 compareBoundaryPoints: compareBoundaryPoints, 2362 deleteContents: deleteContents, 2363 extractContents: extractContents, 2364 cloneContents: cloneContents, 2365 insertNode: insertNode, 2366 surroundContents: surroundContents, 2367 cloneRange: cloneRange, 2368 toStringIE: toStringIE 2369 }); 2370 2371 return self; 2372 } 2373 2374 // Older IE versions doesn't let you override toString by it's constructor so we have to stick it in the prototype 2375 Range.prototype.toString = function() { 2376 return this.toStringIE(); 2377 }; 2378 2379 return Range; 2380 }); 2381 2382 // Included from: js/tinymce/classes/html/Entities.js 2383 2384 /** 2385 * Entities.js 2386 * 2387 * Copyright, Moxiecode Systems AB 2388 * Released under LGPL License. 2389 * 2390 * License: http://www.tinymce.com/license 2391 * Contributing: http://www.tinymce.com/contributing 2392 */ 2393 2394 /*jshint bitwise:false */ 2395 /*eslint no-bitwise:0 */ 2396 2397 /** 2398 * Entity encoder class. 2399 * 2400 * @class tinymce.html.Entities 2401 * @static 2402 * @version 3.4 2403 */ 2404 define("tinymce/html/Entities", [ 2405 "tinymce/util/Tools" 2406 ], function(Tools) { 2407 var makeMap = Tools.makeMap; 2408 2409 var namedEntities, baseEntities, reverseEntities, 2410 attrsCharsRegExp = /[&<>\"\u0060\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g, 2411 textCharsRegExp = /[<>&\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g, 2412 rawCharsRegExp = /[<>&\"\']/g, 2413 entityRegExp = /&(#x|#)?([\w]+);/g, 2414 asciiMap = { 2415 128: "\u20AC", 130: "\u201A", 131: "\u0192", 132: "\u201E", 133: "\u2026", 134: "\u2020", 2416 135: "\u2021", 136: "\u02C6", 137: "\u2030", 138: "\u0160", 139: "\u2039", 140: "\u0152", 2417 142: "\u017D", 145: "\u2018", 146: "\u2019", 147: "\u201C", 148: "\u201D", 149: "\u2022", 2418 150: "\u2013", 151: "\u2014", 152: "\u02DC", 153: "\u2122", 154: "\u0161", 155: "\u203A", 2419 156: "\u0153", 158: "\u017E", 159: "\u0178" 2420 }; 2421 2422 // Raw entities 2423 baseEntities = { 2424 '\"': '"', // Needs to be escaped since the YUI compressor would otherwise break the code 2425 "'": ''', 2426 '<': '<', 2427 '>': '>', 2428 '&': '&', 2429 '\u0060': '`' 2430 }; 2431 2432 // Reverse lookup table for raw entities 2433 reverseEntities = { 2434 '<': '<', 2435 '>': '>', 2436 '&': '&', 2437 '"': '"', 2438 ''': "'" 2439 }; 2440 2441 // Decodes text by using the browser 2442 function nativeDecode(text) { 2443 var elm; 2444 2445 elm = document.createElement("div"); 2446 elm.innerHTML = text; 2447 2448 return elm.textContent || elm.innerText || text; 2449 } 2450 2451 // Build a two way lookup table for the entities 2452 function buildEntitiesLookup(items, radix) { 2453 var i, chr, entity, lookup = {}; 2454 2455 if (items) { 2456 items = items.split(','); 2457 radix = radix || 10; 2458 2459 // Build entities lookup table 2460 for (i = 0; i < items.length; i += 2) { 2461 chr = String.fromCharCode(parseInt(items[i], radix)); 2462 2463 // Only add non base entities 2464 if (!baseEntities[chr]) { 2465 entity = '&' + items[i + 1] + ';'; 2466 lookup[chr] = entity; 2467 lookup[entity] = chr; 2468 } 2469 } 2470 2471 return lookup; 2472 } 2473 } 2474 2475 // Unpack entities lookup where the numbers are in radix 32 to reduce the size 2476 namedEntities = buildEntitiesLookup( 2477 '50,nbsp,51,iexcl,52,cent,53,pound,54,curren,55,yen,56,brvbar,57,sect,58,uml,59,copy,' + 2478 '5a,ordf,5b,laquo,5c,not,5d,shy,5e,reg,5f,macr,5g,deg,5h,plusmn,5i,sup2,5j,sup3,5k,acute,' + 2479 '5l,micro,5m,para,5n,middot,5o,cedil,5p,sup1,5q,ordm,5r,raquo,5s,frac14,5t,frac12,5u,frac34,' + 2480 '5v,iquest,60,Agrave,61,Aacute,62,Acirc,63,Atilde,64,Auml,65,Aring,66,AElig,67,Ccedil,' + 2481 '68,Egrave,69,Eacute,6a,Ecirc,6b,Euml,6c,Igrave,6d,Iacute,6e,Icirc,6f,Iuml,6g,ETH,6h,Ntilde,' + 2482 '6i,Ograve,6j,Oacute,6k,Ocirc,6l,Otilde,6m,Ouml,6n,times,6o,Oslash,6p,Ugrave,6q,Uacute,' + 2483 '6r,Ucirc,6s,Uuml,6t,Yacute,6u,THORN,6v,szlig,70,agrave,71,aacute,72,acirc,73,atilde,74,auml,' + 2484 '75,aring,76,aelig,77,ccedil,78,egrave,79,eacute,7a,ecirc,7b,euml,7c,igrave,7d,iacute,7e,icirc,' + 2485 '7f,iuml,7g,eth,7h,ntilde,7i,ograve,7j,oacute,7k,ocirc,7l,otilde,7m,ouml,7n,divide,7o,oslash,' + 2486 '7p,ugrave,7q,uacute,7r,ucirc,7s,uuml,7t,yacute,7u,thorn,7v,yuml,ci,fnof,sh,Alpha,si,Beta,' + 2487 'sj,Gamma,sk,Delta,sl,Epsilon,sm,Zeta,sn,Eta,so,Theta,sp,Iota,sq,Kappa,sr,Lambda,ss,Mu,' + 2488 'st,Nu,su,Xi,sv,Omicron,t0,Pi,t1,Rho,t3,Sigma,t4,Tau,t5,Upsilon,t6,Phi,t7,Chi,t8,Psi,' + 2489 't9,Omega,th,alpha,ti,beta,tj,gamma,tk,delta,tl,epsilon,tm,zeta,tn,eta,to,theta,tp,iota,' + 2490 'tq,kappa,tr,lambda,ts,mu,tt,nu,tu,xi,tv,omicron,u0,pi,u1,rho,u2,sigmaf,u3,sigma,u4,tau,' + 2491 'u5,upsilon,u6,phi,u7,chi,u8,psi,u9,omega,uh,thetasym,ui,upsih,um,piv,812,bull,816,hellip,' + 2492 '81i,prime,81j,Prime,81u,oline,824,frasl,88o,weierp,88h,image,88s,real,892,trade,89l,alefsym,' + 2493 '8cg,larr,8ch,uarr,8ci,rarr,8cj,darr,8ck,harr,8dl,crarr,8eg,lArr,8eh,uArr,8ei,rArr,8ej,dArr,' + 2494 '8ek,hArr,8g0,forall,8g2,part,8g3,exist,8g5,empty,8g7,nabla,8g8,isin,8g9,notin,8gb,ni,8gf,prod,' + 2495 '8gh,sum,8gi,minus,8gn,lowast,8gq,radic,8gt,prop,8gu,infin,8h0,ang,8h7,and,8h8,or,8h9,cap,8ha,cup,' + 2496 '8hb,int,8hk,there4,8hs,sim,8i5,cong,8i8,asymp,8j0,ne,8j1,equiv,8j4,le,8j5,ge,8k2,sub,8k3,sup,8k4,' + 2497 'nsub,8k6,sube,8k7,supe,8kl,oplus,8kn,otimes,8l5,perp,8m5,sdot,8o8,lceil,8o9,rceil,8oa,lfloor,8ob,' + 2498 'rfloor,8p9,lang,8pa,rang,9ea,loz,9j0,spades,9j3,clubs,9j5,hearts,9j6,diams,ai,OElig,aj,oelig,b0,' + 2499 'Scaron,b1,scaron,bo,Yuml,m6,circ,ms,tilde,802,ensp,803,emsp,809,thinsp,80c,zwnj,80d,zwj,80e,lrm,' + 2500 '80f,rlm,80j,ndash,80k,mdash,80o,lsquo,80p,rsquo,80q,sbquo,80s,ldquo,80t,rdquo,80u,bdquo,810,dagger,' + 2501 '811,Dagger,81g,permil,81p,lsaquo,81q,rsaquo,85c,euro', 32); 2502 2503 var Entities = { 2504 /** 2505 * Encodes the specified string using raw entities. This means only the required XML base entities will be endoded. 2506 * 2507 * @method encodeRaw 2508 * @param {String} text Text to encode. 2509 * @param {Boolean} attr Optional flag to specify if the text is attribute contents. 2510 * @return {String} Entity encoded text. 2511 */ 2512 encodeRaw: function(text, attr) { 2513 return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) { 2514 return baseEntities[chr] || chr; 2515 }); 2516 }, 2517 2518 /** 2519 * Encoded the specified text with both the attributes and text entities. This function will produce larger text contents 2520 * since it doesn't know if the context is within a attribute or text node. This was added for compatibility 2521 * and is exposed as the DOMUtils.encode function. 2522 * 2523 * @method encodeAllRaw 2524 * @param {String} text Text to encode. 2525 * @return {String} Entity encoded text. 2526 */ 2527 encodeAllRaw: function(text) { 2528 return ('' + text).replace(rawCharsRegExp, function(chr) { 2529 return baseEntities[chr] || chr; 2530 }); 2531 }, 2532 2533 /** 2534 * Encodes the specified string using numeric entities. The core entities will be 2535 * encoded as named ones but all non lower ascii characters will be encoded into numeric entities. 2536 * 2537 * @method encodeNumeric 2538 * @param {String} text Text to encode. 2539 * @param {Boolean} attr Optional flag to specify if the text is attribute contents. 2540 * @return {String} Entity encoded text. 2541 */ 2542 encodeNumeric: function(text, attr) { 2543 return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) { 2544 // Multi byte sequence convert it to a single entity 2545 if (chr.length > 1) { 2546 return '' + (((chr.charCodeAt(0) - 0xD800) * 0x400) + (chr.charCodeAt(1) - 0xDC00) + 0x10000) + ';'; 2547 } 2548 2549 return baseEntities[chr] || '' + chr.charCodeAt(0) + ';'; 2550 }); 2551 }, 2552 2553 /** 2554 * Encodes the specified string using named entities. The core entities will be encoded 2555 * as named ones but all non lower ascii characters will be encoded into named entities. 2556 * 2557 * @method encodeNamed 2558 * @param {String} text Text to encode. 2559 * @param {Boolean} attr Optional flag to specify if the text is attribute contents. 2560 * @param {Object} entities Optional parameter with entities to use. 2561 * @return {String} Entity encoded text. 2562 */ 2563 encodeNamed: function(text, attr, entities) { 2564 entities = entities || namedEntities; 2565 2566 return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) { 2567 return baseEntities[chr] || entities[chr] || chr; 2568 }); 2569 }, 2570 2571 /** 2572 * Returns an encode function based on the name(s) and it's optional entities. 2573 * 2574 * @method getEncodeFunc 2575 * @param {String} name Comma separated list of encoders for example named,numeric. 2576 * @param {String} entities Optional parameter with entities to use instead of the built in set. 2577 * @return {function} Encode function to be used. 2578 */ 2579 getEncodeFunc: function(name, entities) { 2580 entities = buildEntitiesLookup(entities) || namedEntities; 2581 2582 function encodeNamedAndNumeric(text, attr) { 2583 return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) { 2584 return baseEntities[chr] || entities[chr] || '' + chr.charCodeAt(0) + ';' || chr; 2585 }); 2586 } 2587 2588 function encodeCustomNamed(text, attr) { 2589 return Entities.encodeNamed(text, attr, entities); 2590 } 2591 2592 // Replace + with , to be compatible with previous TinyMCE versions 2593 name = makeMap(name.replace(/\+/g, ',')); 2594 2595 // Named and numeric encoder 2596 if (name.named && name.numeric) { 2597 return encodeNamedAndNumeric; 2598 } 2599 2600 // Named encoder 2601 if (name.named) { 2602 // Custom names 2603 if (entities) { 2604 return encodeCustomNamed; 2605 } 2606 2607 return Entities.encodeNamed; 2608 } 2609 2610 // Numeric 2611 if (name.numeric) { 2612 return Entities.encodeNumeric; 2613 } 2614 2615 // Raw encoder 2616 return Entities.encodeRaw; 2617 }, 2618 2619 /** 2620 * Decodes the specified string, this will replace entities with raw UTF characters. 2621 * 2622 * @method decode 2623 * @param {String} text Text to entity decode. 2624 * @return {String} Entity decoded string. 2625 */ 2626 decode: function(text) { 2627 return text.replace(entityRegExp, function(all, numeric, value) { 2628 if (numeric) { 2629 value = parseInt(value, numeric.length === 2 ? 16 : 10); 2630 2631 // Support upper UTF 2632 if (value > 0xFFFF) { 2633 value -= 0x10000; 2634 2635 return String.fromCharCode(0xD800 + (value >> 10), 0xDC00 + (value & 0x3FF)); 2636 } else { 2637 return asciiMap[value] || String.fromCharCode(value); 2638 } 2639 } 2640 2641 return reverseEntities[all] || namedEntities[all] || nativeDecode(all); 2642 }); 2643 } 2644 }; 2645 2646 return Entities; 2647 }); 2648 2649 // Included from: js/tinymce/classes/Env.js 2650 2651 /** 2652 * Env.js 2653 * 2654 * Copyright, Moxiecode Systems AB 2655 * Released under LGPL License. 2656 * 2657 * License: http://www.tinymce.com/license 2658 * Contributing: http://www.tinymce.com/contributing 2659 */ 2660 2661 /** 2662 * This class contains various environment constants like browser versions etc. 2663 * Normally you don't want to sniff specific browser versions but sometimes you have 2664 * to when it's impossible to feature detect. So use this with care. 2665 * 2666 * @class tinymce.Env 2667 * @static 2668 */ 2669 define("tinymce/Env", [], function() { 2670 var nav = navigator, userAgent = nav.userAgent; 2671 var opera, webkit, ie, ie11, gecko, mac, iDevice; 2672 2673 opera = window.opera && window.opera.buildNumber; 2674 webkit = /WebKit/.test(userAgent); 2675 ie = !webkit && !opera && (/MSIE/gi).test(userAgent) && (/Explorer/gi).test(nav.appName); 2676 ie = ie && /MSIE (\w+)\./.exec(userAgent)[1]; 2677 ie11 = userAgent.indexOf('Trident/') != -1 && (userAgent.indexOf('rv:') != -1 || nav.appName.indexOf('Netscape') != -1) ? 11 : false; 2678 ie = ie || ie11; 2679 gecko = !webkit && !ie11 && /Gecko/.test(userAgent); 2680 mac = userAgent.indexOf('Mac') != -1; 2681 iDevice = /(iPad|iPhone)/.test(userAgent); 2682 2683 // Is a iPad/iPhone and not on iOS5 sniff the WebKit version since older iOS WebKit versions 2684 // says it has contentEditable support but there is no visible caret. 2685 var contentEditable = !iDevice || userAgent.match(/AppleWebKit\/(\d*)/)[1] >= 534; 2686 2687 return { 2688 /** 2689 * Constant that is true if the browser is Opera. 2690 * 2691 * @property opera 2692 * @type Boolean 2693 * @final 2694 */ 2695 opera: opera, 2696 2697 /** 2698 * Constant that is true if the browser is WebKit (Safari/Chrome). 2699 * 2700 * @property webKit 2701 * @type Boolean 2702 * @final 2703 */ 2704 webkit: webkit, 2705 2706 /** 2707 * Constant that is more than zero if the browser is IE. 2708 * 2709 * @property ie 2710 * @type Boolean 2711 * @final 2712 */ 2713 ie: ie, 2714 2715 /** 2716 * Constant that is true if the browser is Gecko. 2717 * 2718 * @property gecko 2719 * @type Boolean 2720 * @final 2721 */ 2722 gecko: gecko, 2723 2724 /** 2725 * Constant that is true if the os is Mac OS. 2726 * 2727 * @property mac 2728 * @type Boolean 2729 * @final 2730 */ 2731 mac: mac, 2732 2733 /** 2734 * Constant that is true if the os is iOS. 2735 * 2736 * @property iOS 2737 * @type Boolean 2738 * @final 2739 */ 2740 iOS: iDevice, 2741 2742 /** 2743 * Constant that is true if the browser supports editing. 2744 * 2745 * @property contentEditable 2746 * @type Boolean 2747 * @final 2748 */ 2749 contentEditable: contentEditable, 2750 2751 /** 2752 * Transparent image data url. 2753 * 2754 * @property transparentSrc 2755 * @type Boolean 2756 * @final 2757 */ 2758 transparentSrc: "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7", 2759 2760 /** 2761 * Returns true/false if the browser can or can't place the caret after a inline block like an image. 2762 * 2763 * @property noCaretAfter 2764 * @type Boolean 2765 * @final 2766 */ 2767 caretAfter: ie != 8, 2768 2769 /** 2770 * Constant that is true if the browser supports native DOM Ranges. IE 9+. 2771 * 2772 * @property range 2773 * @type Boolean 2774 */ 2775 range: window.getSelection && "Range" in window, 2776 2777 /** 2778 * Returns the IE document mode for non IE browsers this will fake IE 10. 2779 * 2780 * @property documentMode 2781 * @type Number 2782 */ 2783 documentMode: ie ? (document.documentMode || 7) : 10 2784 }; 2785 }); 2786 2787 // Included from: js/tinymce/classes/dom/StyleSheetLoader.js 2788 2789 /** 2790 * StyleSheetLoader.js 2791 * 2792 * Copyright, Moxiecode Systems AB 2793 * Released under LGPL License. 2794 * 2795 * License: http://www.tinymce.com/license 2796 * Contributing: http://www.tinymce.com/contributing 2797 */ 2798 2799 /** 2800 * This class handles loading of external stylesheets and fires events when these are loaded. 2801 * 2802 * @class tinymce.dom.StyleSheetLoader 2803 * @private 2804 */ 2805 define("tinymce/dom/StyleSheetLoader", [], function() { 2806 "use strict"; 2807 2808 return function(document, settings) { 2809 var idCount = 0, loadedStates = {}, maxLoadTime; 2810 2811 settings = settings || {}; 2812 maxLoadTime = settings.maxLoadTime || 5000; 2813 2814 function appendToHead(node) { 2815 document.getElementsByTagName('head')[0].appendChild(node); 2816 } 2817 2818 /** 2819 * Loads the specified css style sheet file and call the loadedCallback once it's finished loading. 2820 * 2821 * @method load 2822 * @param {String} url Url to be loaded. 2823 * @param {Function} loadedCallback Callback to be executed when loaded. 2824 * @param {Function} errorCallback Callback to be executed when failed loading. 2825 */ 2826 function load(url, loadedCallback, errorCallback) { 2827 var link, style, startTime, state; 2828 2829 function passed() { 2830 var callbacks = state.passed, i = callbacks.length; 2831 2832 while (i--) { 2833 callbacks[i](); 2834 } 2835 2836 state.status = 2; 2837 state.passed = []; 2838 state.failed = []; 2839 } 2840 2841 function failed() { 2842 var callbacks = state.failed, i = callbacks.length; 2843 2844 while (i--) { 2845 callbacks[i](); 2846 } 2847 2848 state.status = 3; 2849 state.passed = []; 2850 state.failed = []; 2851 } 2852 2853 // Sniffs for older WebKit versions that have the link.onload but a broken one 2854 function isOldWebKit() { 2855 var webKitChunks = navigator.userAgent.match(/WebKit\/(\d*)/); 2856 return !!(webKitChunks && webKitChunks[1] < 536); 2857 } 2858 2859 // Calls the waitCallback until the test returns true or the timeout occurs 2860 function wait(testCallback, waitCallback) { 2861 if (!testCallback()) { 2862 // Wait for timeout 2863 if ((new Date().getTime()) - startTime < maxLoadTime) { 2864 window.setTimeout(waitCallback, 0); 2865 } else { 2866 failed(); 2867 } 2868 } 2869 } 2870 2871 // Workaround for WebKit that doesn't properly support the onload event for link elements 2872 // Or WebKit that fires the onload event before the StyleSheet is added to the document 2873 function waitForWebKitLinkLoaded() { 2874 wait(function() { 2875 var styleSheets = document.styleSheets, styleSheet, i = styleSheets.length, owner; 2876 2877 while (i--) { 2878 styleSheet = styleSheets[i]; 2879 owner = styleSheet.ownerNode ? styleSheet.ownerNode : styleSheet.owningElement; 2880 if (owner && owner.id === link.id) { 2881 passed(); 2882 return true; 2883 } 2884 } 2885 }, waitForWebKitLinkLoaded); 2886 } 2887 2888 // Workaround for older Geckos that doesn't have any onload event for StyleSheets 2889 function waitForGeckoLinkLoaded() { 2890 wait(function() { 2891 try { 2892 // Accessing the cssRules will throw an exception until the CSS file is loaded 2893 var cssRules = style.sheet.cssRules; 2894 passed(); 2895 return !!cssRules; 2896 } catch (ex) { 2897 // Ignore 2898 } 2899 }, waitForGeckoLinkLoaded); 2900 } 2901 2902 if (!loadedStates[url]) { 2903 state = { 2904 passed: [], 2905 failed: [] 2906 }; 2907 2908 loadedStates[url] = state; 2909 } else { 2910 state = loadedStates[url]; 2911 } 2912 2913 if (loadedCallback) { 2914 state.passed.push(loadedCallback); 2915 } 2916 2917 if (errorCallback) { 2918 state.failed.push(errorCallback); 2919 } 2920 2921 // Is loading wait for it to pass 2922 if (state.status == 1) { 2923 return; 2924 } 2925 2926 // Has finished loading and was success 2927 if (state.status == 2) { 2928 passed(); 2929 return; 2930 } 2931 2932 // Has finished loading and was a failure 2933 if (state.status == 3) { 2934 failed(); 2935 return; 2936 } 2937 2938 // Start loading 2939 state.status = 1; 2940 link = document.createElement('link'); 2941 link.rel = 'stylesheet'; 2942 link.type = 'text/css'; 2943 link.id = 'u' + (idCount++); 2944 link.async = false; 2945 link.defer = false; 2946 startTime = new Date().getTime(); 2947 2948 // Feature detect onload on link element and sniff older webkits since it has an broken onload event 2949 if ("onload" in link && !isOldWebKit()) { 2950 link.onload = waitForWebKitLinkLoaded; 2951 link.onerror = failed; 2952 } else { 2953 // Sniff for old Firefox that doesn't support the onload event on link elements 2954 // TODO: Remove this in the future when everyone uses modern browsers 2955 if (navigator.userAgent.indexOf("Firefox") > 0) { 2956 style = document.createElement('style'); 2957 style.textContent = '@import "' + url + '"'; 2958 waitForGeckoLinkLoaded(); 2959 appendToHead(style); 2960 return; 2961 } else { 2962 // Use the id owner on older webkits 2963 waitForWebKitLinkLoaded(); 2964 } 2965 } 2966 2967 appendToHead(link); 2968 link.href = url; 2969 } 2970 2971 this.load = load; 2972 }; 2973 }); 2974 2975 // Included from: js/tinymce/classes/dom/DOMUtils.js 2976 2977 /** 2978 * DOMUtils.js 2979 * 2980 * Copyright, Moxiecode Systems AB 2981 * Released under LGPL License. 2982 * 2983 * License: http://www.tinymce.com/license 2984 * Contributing: http://www.tinymce.com/contributing 2985 */ 2986 2987 /** 2988 * Utility class for various DOM manipulation and retrieval functions. 2989 * 2990 * @class tinymce.dom.DOMUtils 2991 * @example 2992 * // Add a class to an element by id in the page 2993 * tinymce.DOM.addClass('someid', 'someclass'); 2994 * 2995 * // Add a class to an element by id inside the editor 2996 * tinymce.activeEditor.dom.addClass('someid', 'someclass'); 2997 */ 2998 define("tinymce/dom/DOMUtils", [ 2999 "tinymce/dom/Sizzle", 3000 "tinymce/html/Styles", 3001 "tinymce/dom/EventUtils", 3002 "tinymce/dom/TreeWalker", 3003 "tinymce/dom/Range", 3004 "tinymce/html/Entities", 3005 "tinymce/Env", 3006 "tinymce/util/Tools", 3007 "tinymce/dom/StyleSheetLoader" 3008 ], function(Sizzle, Styles, EventUtils, TreeWalker, Range, Entities, Env, Tools, StyleSheetLoader) { 3009 // Shorten names 3010 var each = Tools.each, is = Tools.is, grep = Tools.grep, trim = Tools.trim, extend = Tools.extend; 3011 var isWebKit = Env.webkit, isIE = Env.ie; 3012 var simpleSelectorRe = /^([a-z0-9],?)+$/i; 3013 var whiteSpaceRegExp = /^[ \t\r\n]*$/; 3014 var numericCssMap = Tools.makeMap('fillOpacity fontWeight lineHeight opacity orphans widows zIndex zoom', ' '); 3015 3016 /** 3017 * Constructs a new DOMUtils instance. Consult the Wiki for more details on settings etc for this class. 3018 * 3019 * @constructor 3020 * @method DOMUtils 3021 * @param {Document} d Document reference to bind the utility class to. 3022 * @param {settings} s Optional settings collection. 3023 */ 3024 function DOMUtils(doc, settings) { 3025 var self = this, blockElementsMap; 3026 3027 self.doc = doc; 3028 self.win = window; 3029 self.files = {}; 3030 self.counter = 0; 3031 self.stdMode = !isIE || doc.documentMode >= 8; 3032 self.boxModel = !isIE || doc.compatMode == "CSS1Compat" || self.stdMode; 3033 self.hasOuterHTML = "outerHTML" in doc.createElement("a"); 3034 self.styleSheetLoader = new StyleSheetLoader(doc); 3035 this.boundEvents = []; 3036 3037 self.settings = settings = extend({ 3038 keep_values: false, 3039 hex_colors: 1 3040 }, settings); 3041 3042 self.schema = settings.schema; 3043 self.styles = new Styles({ 3044 url_converter: settings.url_converter, 3045 url_converter_scope: settings.url_converter_scope 3046 }, settings.schema); 3047 3048 self.fixDoc(doc); 3049 self.events = settings.ownEvents ? new EventUtils(settings.proxy) : EventUtils.Event; 3050 blockElementsMap = settings.schema ? settings.schema.getBlockElements() : {}; 3051 3052 /** 3053 * Returns true/false if the specified element is a block element or not. 3054 * 3055 * @method isBlock 3056 * @param {Node/String} node Element/Node to check. 3057 * @return {Boolean} True/False state if the node is a block element or not. 3058 */ 3059 self.isBlock = function(node) { 3060 // Fix for #5446 3061 if (!node) { 3062 return false; 3063 } 3064 3065 // This function is called in module pattern style since it might be executed with the wrong this scope 3066 var type = node.nodeType; 3067 3068 // If it's a node then check the type and use the nodeName 3069 if (type) { 3070 return !!(type === 1 && blockElementsMap[node.nodeName]); 3071 } 3072 3073 return !!blockElementsMap[node]; 3074 }; 3075 } 3076 3077 DOMUtils.prototype = { 3078 root: null, 3079 props: { 3080 "for": "htmlFor", 3081 "class": "className", 3082 className: "className", 3083 checked: "checked", 3084 disabled: "disabled", 3085 maxlength: "maxLength", 3086 readonly: "readOnly", 3087 selected: "selected", 3088 value: "value", 3089 id: "id", 3090 name: "name", 3091 type: "type" 3092 }, 3093 3094 fixDoc: function(doc) { 3095 var settings = this.settings, name; 3096 3097 if (isIE && settings.schema) { 3098 // Add missing HTML 4/5 elements to IE 3099 ('abbr article aside audio canvas ' + 3100 'details figcaption figure footer ' + 3101 'header hgroup mark menu meter nav ' + 3102 'output progress section summary ' + 3103 'time video').replace(/\w+/g, function(name) { 3104 doc.createElement(name); 3105 }); 3106 3107 // Create all custom elements 3108 for (name in settings.schema.getCustomElements()) { 3109 doc.createElement(name); 3110 } 3111 } 3112 }, 3113 3114 clone: function(node, deep) { 3115 var self = this, clone, doc; 3116 3117 // TODO: Add feature detection here in the future 3118 if (!isIE || node.nodeType !== 1 || deep) { 3119 return node.cloneNode(deep); 3120 } 3121 3122 doc = self.doc; 3123 3124 // Make a HTML5 safe shallow copy 3125 if (!deep) { 3126 clone = doc.createElement(node.nodeName); 3127 3128 // Copy attribs 3129 each(self.getAttribs(node), function(attr) { 3130 self.setAttrib(clone, attr.nodeName, self.getAttrib(node, attr.nodeName)); 3131 }); 3132 3133 return clone; 3134 } 3135 /* 3136 // Setup HTML5 patched document fragment 3137 if (!self.frag) { 3138 self.frag = doc.createDocumentFragment(); 3139 self.fixDoc(self.frag); 3140 } 3141 3142 // Make a deep copy by adding it to the document fragment then removing it this removed the :section 3143 clone = doc.createElement('div'); 3144 self.frag.appendChild(clone); 3145 clone.innerHTML = node.outerHTML; 3146 self.frag.removeChild(clone); 3147 */ 3148 return clone.firstChild; 3149 }, 3150 3151 /** 3152 * Returns the root node of the document. This is normally the body but might be a DIV. Parents like getParent will not 3153 * go above the point of this root node. 3154 * 3155 * @method getRoot 3156 * @return {Element} Root element for the utility class. 3157 */ 3158 getRoot: function() { 3159 var self = this; 3160 3161 return self.get(self.settings.root_element) || self.doc.body; 3162 }, 3163 3164 /** 3165 * Returns the viewport of the window. 3166 * 3167 * @method getViewPort 3168 * @param {Window} win Optional window to get viewport of. 3169 * @return {Object} Viewport object with fields x, y, w and h. 3170 */ 3171 getViewPort: function(win) { 3172 var doc, rootElm; 3173 3174 win = !win ? this.win : win; 3175 doc = win.document; 3176 rootElm = this.boxModel ? doc.documentElement : doc.body; 3177 3178 // Returns viewport size excluding scrollbars 3179 return { 3180 x: win.pageXOffset || rootElm.scrollLeft, 3181 y: win.pageYOffset || rootElm.scrollTop, 3182 w: win.innerWidth || rootElm.clientWidth, 3183 h: win.innerHeight || rootElm.clientHeight 3184 }; 3185 }, 3186 3187 /** 3188 * Returns the rectangle for a specific element. 3189 * 3190 * @method getRect 3191 * @param {Element/String} elm Element object or element ID to get rectangle from. 3192 * @return {object} Rectangle for specified element object with x, y, w, h fields. 3193 */ 3194 getRect: function(elm) { 3195 var self = this, pos, size; 3196 3197 elm = self.get(elm); 3198 pos = self.getPos(elm); 3199 size = self.getSize(elm); 3200 3201 return { 3202 x: pos.x, y: pos.y, 3203 w: size.w, h: size.h 3204 }; 3205 }, 3206 3207 /** 3208 * Returns the size dimensions of the specified element. 3209 * 3210 * @method getSize 3211 * @param {Element/String} elm Element object or element ID to get rectangle from. 3212 * @return {object} Rectangle for specified element object with w, h fields. 3213 */ 3214 getSize: function(elm) { 3215 var self = this, w, h; 3216 3217 elm = self.get(elm); 3218 w = self.getStyle(elm, 'width'); 3219 h = self.getStyle(elm, 'height'); 3220 3221 // Non pixel value, then force offset/clientWidth 3222 if (w.indexOf('px') === -1) { 3223 w = 0; 3224 } 3225 3226 // Non pixel value, then force offset/clientWidth 3227 if (h.indexOf('px') === -1) { 3228 h = 0; 3229 } 3230 3231 return { 3232 w: parseInt(w, 10) || elm.offsetWidth || elm.clientWidth, 3233 h: parseInt(h, 10) || elm.offsetHeight || elm.clientHeight 3234 }; 3235 }, 3236 3237 /** 3238 * Returns a node by the specified selector function. This function will 3239 * loop through all parent nodes and call the specified function for each node. 3240 * If the function then returns true indicating that it has found what it was looking for, the loop execution will then end 3241 * and the node it found will be returned. 3242 * 3243 * @method getParent 3244 * @param {Node/String} node DOM node to search parents on or ID string. 3245 * @param {function} selector Selection function or CSS selector to execute on each node. 3246 * @param {Node} root Optional root element, never go below this point. 3247 * @return {Node} DOM Node or null if it wasn't found. 3248 */ 3249 getParent: function(node, selector, root) { 3250 return this.getParents(node, selector, root, false); 3251 }, 3252 3253 /** 3254 * Returns a node list of all parents matching the specified selector function or pattern. 3255 * If the function then returns true indicating that it has found what it was looking for and that node will be collected. 3256 * 3257 * @method getParents 3258 * @param {Node/String} node DOM node to search parents on or ID string. 3259 * @param {function} selector Selection function to execute on each node or CSS pattern. 3260 * @param {Node} root Optional root element, never go below this point. 3261 * @return {Array} Array of nodes or null if it wasn't found. 3262 */ 3263 getParents: function(node, selector, root, collect) { 3264 var self = this, selectorVal, result = []; 3265 3266 node = self.get(node); 3267 collect = collect === undefined; 3268 3269 // Default root on inline mode 3270 root = root || (self.getRoot().nodeName != 'BODY' ? self.getRoot().parentNode : null); 3271 3272 // Wrap node name as func 3273 if (is(selector, 'string')) { 3274 selectorVal = selector; 3275 3276 if (selector === '*') { 3277 selector = function(node) {return node.nodeType == 1;}; 3278 } else { 3279 selector = function(node) { 3280 return self.is(node, selectorVal); 3281 }; 3282 } 3283 } 3284 3285 while (node) { 3286 if (node == root || !node.nodeType || node.nodeType === 9) { 3287 break; 3288 } 3289 3290 if (!selector || selector(node)) { 3291 if (collect) { 3292 result.push(node); 3293 } else { 3294 return node; 3295 } 3296 } 3297 3298 node = node.parentNode; 3299 } 3300 3301 return collect ? result : null; 3302 }, 3303 3304 /** 3305 * Returns the specified element by ID or the input element if it isn't a string. 3306 * 3307 * @method get 3308 * @param {String/Element} n Element id to look for or element to just pass though. 3309 * @return {Element} Element matching the specified id or null if it wasn't found. 3310 */ 3311 get: function(elm) { 3312 var name; 3313 3314 if (elm && this.doc && typeof(elm) == 'string') { 3315 name = elm; 3316 elm = this.doc.getElementById(elm); 3317 3318 // IE and Opera returns meta elements when they match the specified input ID, but getElementsByName seems to do the trick 3319 if (elm && elm.id !== name) { 3320 return this.doc.getElementsByName(name)[1]; 3321 } 3322 } 3323 3324 return elm; 3325 }, 3326 3327 /** 3328 * Returns the next node that matches selector or function 3329 * 3330 * @method getNext 3331 * @param {Node} node Node to find siblings from. 3332 * @param {String/function} selector Selector CSS expression or function. 3333 * @return {Node} Next node item matching the selector or null if it wasn't found. 3334 */ 3335 getNext: function(node, selector) { 3336 return this._findSib(node, selector, 'nextSibling'); 3337 }, 3338 3339 /** 3340 * Returns the previous node that matches selector or function 3341 * 3342 * @method getPrev 3343 * @param {Node} node Node to find siblings from. 3344 * @param {String/function} selector Selector CSS expression or function. 3345 * @return {Node} Previous node item matching the selector or null if it wasn't found. 3346 */ 3347 getPrev: function(node, selector) { 3348 return this._findSib(node, selector, 'previousSibling'); 3349 }, 3350 3351 // #ifndef jquery 3352 3353 /** 3354 * Selects specific elements by a CSS level 3 pattern. For example "div#a1 p.test". 3355 * This function is optimized for the most common patterns needed in TinyMCE but it also performs well enough 3356 * on more complex patterns. 3357 * 3358 * @method select 3359 * @param {String} selector CSS level 3 pattern to select/find elements by. 3360 * @param {Object} scope Optional root element/scope element to search in. 3361 * @return {Array} Array with all matched elements. 3362 * @example 3363 * // Adds a class to all paragraphs in the currently active editor 3364 * tinymce.activeEditor.dom.addClass(tinymce.activeEditor.dom.select('p'), 'someclass'); 3365 * 3366 * // Adds a class to all spans that have the test class in the currently active editor 3367 * tinymce.activeEditor.dom.addClass(tinymce.activeEditor.dom.select('span.test'), 'someclass') 3368 */ 3369 select: function(selector, scope) { 3370 var self = this; 3371 3372 //Sizzle.selectors.cacheLength = 0; 3373 return Sizzle(selector, self.get(scope) || self.get(self.settings.root_element) || self.doc, []); 3374 }, 3375 3376 /** 3377 * Returns true/false if the specified element matches the specified css pattern. 3378 * 3379 * @method is 3380 * @param {Node/NodeList} elm DOM node to match or an array of nodes to match. 3381 * @param {String} selector CSS pattern to match the element against. 3382 */ 3383 is: function(elm, selector) { 3384 var i; 3385 3386 // If it isn't an array then try to do some simple selectors instead of Sizzle for to boost performance 3387 if (elm.length === undefined) { 3388 // Simple all selector 3389 if (selector === '*') { 3390 return elm.nodeType == 1; 3391 } 3392 3393 // Simple selector just elements 3394 if (simpleSelectorRe.test(selector)) { 3395 selector = selector.toLowerCase().split(/,/); 3396 elm = elm.nodeName.toLowerCase(); 3397 3398 for (i = selector.length - 1; i >= 0; i--) { 3399 if (selector[i] == elm) { 3400 return true; 3401 } 3402 } 3403 3404 return false; 3405 } 3406 } 3407 3408 // Is non element 3409 if (elm.nodeType && elm.nodeType != 1) { 3410 return false; 3411 } 3412 3413 var elms = elm.nodeType ? [elm] : elm; 3414 return Sizzle(selector, elms[0].ownerDocument || elms[0], null, elms).length > 0; 3415 }, 3416 3417 // #endif 3418 3419 /** 3420 * Adds the specified element to another element or elements. 3421 * 3422 * @method add 3423 * @param {String/Element/Array} parentElm Element id string, DOM node element or array of ids or elements to add to. 3424 * @param {String/Element} name Name of new element to add or existing element to add. 3425 * @param {Object} attrs Optional object collection with arguments to add to the new element(s). 3426 * @param {String} html Optional inner HTML contents to add for each element. 3427 * @return {Element/Array} Element that got created, or an array of created elements if multiple input elements 3428 * were passed in. 3429 * @example 3430 * // Adds a new paragraph to the end of the active editor 3431 * tinymce.activeEditor.dom.add(tinymce.activeEditor.getBody(), 'p', {title: 'my title'}, 'Some content'); 3432 */ 3433 add: function(parentElm, name, attrs, html, create) { 3434 var self = this; 3435 3436 return this.run(parentElm, function(parentElm) { 3437 var newElm; 3438 3439 newElm = is(name, 'string') ? self.doc.createElement(name) : name; 3440 self.setAttribs(newElm, attrs); 3441 3442 if (html) { 3443 if (html.nodeType) { 3444 newElm.appendChild(html); 3445 } else { 3446 self.setHTML(newElm, html); 3447 } 3448 } 3449 3450 return !create ? parentElm.appendChild(newElm) : newElm; 3451 }); 3452 }, 3453 3454 /** 3455 * Creates a new element. 3456 * 3457 * @method create 3458 * @param {String} name Name of new element. 3459 * @param {Object} attrs Optional object name/value collection with element attributes. 3460 * @param {String} html Optional HTML string to set as inner HTML of the element. 3461 * @return {Element} HTML DOM node element that got created. 3462 * @example 3463 * // Adds an element where the caret/selection is in the active editor 3464 * var el = tinymce.activeEditor.dom.create('div', {id: 'test', 'class': 'myclass'}, 'some content'); 3465 * tinymce.activeEditor.selection.setNode(el); 3466 */ 3467 create: function(name, attrs, html) { 3468 return this.add(this.doc.createElement(name), name, attrs, html, 1); 3469 }, 3470 3471 /** 3472 * Creates HTML string for element. The element will be closed unless an empty inner HTML string is passed in. 3473 * 3474 * @method createHTML 3475 * @param {String} name Name of new element. 3476 * @param {Object} attrs Optional object name/value collection with element attributes. 3477 * @param {String} html Optional HTML string to set as inner HTML of the element. 3478 * @return {String} String with new HTML element, for example: <a href="#">test</a>. 3479 * @example 3480 * // Creates a html chunk and inserts it at the current selection/caret location 3481 * tinymce.activeEditor.selection.setContent(tinymce.activeEditor.dom.createHTML('a', {href: 'test.html'}, 'some line')); 3482 */ 3483 createHTML: function(name, attrs, html) { 3484 var outHtml = '', key; 3485 3486 outHtml += '<' + name; 3487 3488 for (key in attrs) { 3489 if (attrs.hasOwnProperty(key) && attrs[key] !== null) { 3490 outHtml += ' ' + key + '="' + this.encode(attrs[key]) + '"'; 3491 } 3492 } 3493 3494 // A call to tinymce.is doesn't work for some odd reason on IE9 possible bug inside their JS runtime 3495 if (typeof(html) != "undefined") { 3496 return outHtml + '>' + html + '</' + name + '>'; 3497 } 3498 3499 return outHtml + ' />'; 3500 }, 3501 3502 /** 3503 * Creates a document fragment out of the specified HTML string. 3504 * 3505 * @method createFragment 3506 * @param {String} html Html string to create fragment from. 3507 * @return {DocumentFragment} Document fragment node. 3508 */ 3509 createFragment: function(html) { 3510 var frag, node, doc = this.doc, container; 3511 3512 container = doc.createElement("div"); 3513 frag = doc.createDocumentFragment(); 3514 3515 if (html) { 3516 container.innerHTML = html; 3517 } 3518 3519 while ((node = container.firstChild)) { 3520 frag.appendChild(node); 3521 } 3522 3523 return frag; 3524 }, 3525 3526 /** 3527 * Removes/deletes the specified element(s) from the DOM. 3528 * 3529 * @method remove 3530 * @param {String/Element/Array} node ID of element or DOM element object or array containing multiple elements/ids. 3531 * @param {Boolean} keep_children Optional state to keep children or not. If set to true all children will be 3532 * placed at the location of the removed element. 3533 * @return {Element/Array} HTML DOM element that got removed, or an array of removed elements if multiple input elements 3534 * were passed in. 3535 * @example 3536 * // Removes all paragraphs in the active editor 3537 * tinymce.activeEditor.dom.remove(tinymce.activeEditor.dom.select('p')); 3538 * 3539 * // Removes an element by id in the document 3540 * tinymce.DOM.remove('mydiv'); 3541 */ 3542 remove: function(node, keep_children) { 3543 return this.run(node, function(node) { 3544 var child, parent = node.parentNode; 3545 3546 if (!parent) { 3547 return null; 3548 } 3549 3550 if (keep_children) { 3551 while ((child = node.firstChild)) { 3552 // IE 8 will crash if you don't remove completely empty text nodes 3553 if (!isIE || child.nodeType !== 3 || child.nodeValue) { 3554 parent.insertBefore(child, node); 3555 } else { 3556 node.removeChild(child); 3557 } 3558 } 3559 } 3560 3561 return parent.removeChild(node); 3562 }); 3563 }, 3564 3565 /** 3566 * Sets the CSS style value on a HTML element. The name can be a camelcase string 3567 * or the CSS style name like background-color. 3568 * 3569 * @method setStyle 3570 * @param {String/Element/Array} n HTML element/Element ID or Array of elements/ids to set CSS style value on. 3571 * @param {String} na Name of the style value to set. 3572 * @param {String} v Value to set on the style. 3573 * @example 3574 * // Sets a style value on all paragraphs in the currently active editor 3575 * tinymce.activeEditor.dom.setStyle(tinymce.activeEditor.dom.select('p'), 'background-color', 'red'); 3576 * 3577 * // Sets a style value to an element by id in the current document 3578 * tinymce.DOM.setStyle('mydiv', 'background-color', 'red'); 3579 */ 3580 setStyle: function(elm, name, value) { 3581 return this.run(elm, function(elm) { 3582 var self = this, style, key; 3583 3584 if (name) { 3585 if (typeof(name) === 'string') { 3586 style = elm.style; 3587 3588 // Camelcase it, if needed 3589 name = name.replace(/-(\D)/g, function(a, b) { 3590 return b.toUpperCase(); 3591 }); 3592 3593 // Default px suffix on these 3594 if (typeof(value) === 'number' && !numericCssMap[name]) { 3595 value += 'px'; 3596 } 3597 3598 // IE specific opacity 3599 if (name === "opacity" && elm.runtimeStyle && typeof(elm.runtimeStyle.opacity) === "undefined") { 3600 style.filter = value === '' ? '' : "alpha(opacity=" + (value * 100) + ")"; 3601 } 3602 3603 if (name == "float") { 3604 // Old IE vs modern browsers 3605 name = "cssFloat" in elm.style ? "cssFloat" : "styleFloat"; 3606 } 3607 3608 try { 3609 style[name] = value; 3610 } catch (ex) { 3611 // Ignore IE errors 3612 } 3613 3614 // Force update of the style data 3615 if (self.settings.update_styles) { 3616 elm.removeAttribute('data-mce-style'); 3617 } 3618 } else { 3619 for (key in name) { 3620 self.setStyle(elm, key, name[key]); 3621 } 3622 } 3623 } 3624 }); 3625 }, 3626 3627 /** 3628 * Returns the current style or runtime/computed value of an element. 3629 * 3630 * @method getStyle 3631 * @param {String/Element} elm HTML element or element id string to get style from. 3632 * @param {String} name Style name to return. 3633 * @param {Boolean} computed Computed style. 3634 * @return {String} Current style or computed style value of an element. 3635 */ 3636 getStyle: function(elm, name, computed) { 3637 elm = this.get(elm); 3638 3639 if (!elm) { 3640 return; 3641 } 3642 3643 // W3C 3644 if (this.doc.defaultView && computed) { 3645 // Remove camelcase 3646 name = name.replace(/[A-Z]/g, function(a){ 3647 return '-' + a; 3648 }); 3649 3650 try { 3651 return this.doc.defaultView.getComputedStyle(elm, null).getPropertyValue(name); 3652 } catch (ex) { 3653 // Old safari might fail 3654 return null; 3655 } 3656 } 3657 3658 // Camelcase it, if needed 3659 name = name.replace(/-(\D)/g, function(a, b) { 3660 return b.toUpperCase(); 3661 }); 3662 3663 if (name == 'float') { 3664 name = isIE ? 'styleFloat' : 'cssFloat'; 3665 } 3666 3667 // IE & Opera 3668 if (elm.currentStyle && computed) { 3669 return elm.currentStyle[name]; 3670 } 3671 3672 return elm.style ? elm.style[name] : undefined; 3673 }, 3674 3675 /** 3676 * Sets multiple styles on the specified element(s). 3677 * 3678 * @method setStyles 3679 * @param {Element/String/Array} e DOM element, element id string or array of elements/ids to set styles on. 3680 * @param {Object} o Name/Value collection of style items to add to the element(s). 3681 * @example 3682 * // Sets styles on all paragraphs in the currently active editor 3683 * tinymce.activeEditor.dom.setStyles(tinymce.activeEditor.dom.select('p'), {'background-color': 'red', 'color': 'green'}); 3684 * 3685 * // Sets styles to an element by id in the current document 3686 * tinymce.DOM.setStyles('mydiv', {'background-color': 'red', 'color': 'green'}); 3687 */ 3688 setStyles: function(elm, styles) { 3689 this.setStyle(elm, styles); 3690 }, 3691 3692 css: function(elm, name, value) { 3693 this.setStyle(elm, name, value); 3694 }, 3695 3696 /** 3697 * Removes all attributes from an element or elements. 3698 * 3699 * @method removeAllAttribs 3700 * @param {Element/String/Array} e DOM element, element id string or array of elements/ids to remove attributes from. 3701 */ 3702 removeAllAttribs: function(e) { 3703 return this.run(e, function(e) { 3704 var i, attrs = e.attributes; 3705 for (i = attrs.length - 1; i >= 0; i--) { 3706 e.removeAttributeNode(attrs.item(i)); 3707 } 3708 }); 3709 }, 3710 3711 /** 3712 * Sets the specified attribute of an element or elements. 3713 * 3714 * @method setAttrib 3715 * @param {Element/String/Array} e DOM element, element id string or array of elements/ids to set attribute on. 3716 * @param {String} n Name of attribute to set. 3717 * @param {String} v Value to set on the attribute - if this value is falsy like null, 0 or '' it will remove the attribute instead. 3718 * @example 3719 * // Sets class attribute on all paragraphs in the active editor 3720 * tinymce.activeEditor.dom.setAttrib(tinymce.activeEditor.dom.select('p'), 'class', 'myclass'); 3721 * 3722 * // Sets class attribute on a specific element in the current page 3723 * tinymce.dom.setAttrib('mydiv', 'class', 'myclass'); 3724 */ 3725 setAttrib: function(e, n, v) { 3726 var self = this; 3727 3728 // What's the point 3729 if (!e || !n) { 3730 return; 3731 } 3732 3733 return this.run(e, function(e) { 3734 var s = self.settings; 3735 var originalValue = e.getAttribute(n); 3736 if (v !== null) { 3737 switch (n) { 3738 case "style": 3739 if (!is(v, 'string')) { 3740 each(v, function(v, n) { 3741 self.setStyle(e, n, v); 3742 }); 3743 3744 return; 3745 } 3746 3747 // No mce_style for elements with these since they might get resized by the user 3748 if (s.keep_values) { 3749 if (v) { 3750 e.setAttribute('data-mce-style', v, 2); 3751 } else { 3752 e.removeAttribute('data-mce-style', 2); 3753 } 3754 } 3755 3756 e.style.cssText = v; 3757 break; 3758 3759 case "class": 3760 e.className = v || ''; // Fix IE null bug 3761 break; 3762 3763 case "src": 3764 case "href": 3765 if (s.keep_values) { 3766 if (s.url_converter) { 3767 v = s.url_converter.call(s.url_converter_scope || self, v, n, e); 3768 } 3769 3770 self.setAttrib(e, 'data-mce-' + n, v, 2); 3771 } 3772 3773 break; 3774 3775 case "shape": 3776 e.setAttribute('data-mce-style', v); 3777 break; 3778 } 3779 } 3780 if (is(v) && v !== null && v.length !== 0) { 3781 e.setAttribute(n, '' + v, 2); 3782 } else { 3783 e.removeAttribute(n, 2); 3784 } 3785 3786 // fire onChangeAttrib event for attributes that have changed 3787 if (originalValue != v && s.onSetAttrib) { 3788 s.onSetAttrib({attrElm: e, attrName: n, attrValue: v}); 3789 } 3790 }); 3791 }, 3792 3793 /** 3794 * Sets two or more specified attributes of an element or elements. 3795 * 3796 * @method setAttribs 3797 * @param {Element/String/Array} elm DOM element, element id string or array of elements/ids to set attributes on. 3798 * @param {Object} attrs Name/Value collection of attribute items to add to the element(s). 3799 * @example 3800 * // Sets class and title attributes on all paragraphs in the active editor 3801 * tinymce.activeEditor.dom.setAttribs(tinymce.activeEditor.dom.select('p'), {'class': 'myclass', title: 'some title'}); 3802 * 3803 * // Sets class and title attributes on a specific element in the current page 3804 * tinymce.DOM.setAttribs('mydiv', {'class': 'myclass', title: 'some title'}); 3805 */ 3806 setAttribs: function(elm, attrs) { 3807 var self = this; 3808 3809 return this.run(elm, function(elm) { 3810 each(attrs, function(value, name) { 3811 self.setAttrib(elm, name, value); 3812 }); 3813 }); 3814 }, 3815 3816 /** 3817 * Returns the specified attribute by name. 3818 * 3819 * @method getAttrib 3820 * @param {String/Element} elm Element string id or DOM element to get attribute from. 3821 * @param {String} name Name of attribute to get. 3822 * @param {String} defaultVal Optional default value to return if the attribute didn't exist. 3823 * @return {String} Attribute value string, default value or null if the attribute wasn't found. 3824 */ 3825 getAttrib: function(elm, name, defaultVal) { 3826 var value, self = this, undef; 3827 3828 elm = self.get(elm); 3829 3830 if (!elm || elm.nodeType !== 1) { 3831 return defaultVal === undef ? false : defaultVal; 3832 } 3833 3834 if (!is(defaultVal)) { 3835 defaultVal = ''; 3836 } 3837 3838 // Try the mce variant for these 3839 if (/^(src|href|style|coords|shape)$/.test(name)) { 3840 value = elm.getAttribute("data-mce-" + name); 3841 3842 if (value) { 3843 return value; 3844 } 3845 } 3846 3847 if (isIE && self.props[name]) { 3848 value = elm[self.props[name]]; 3849 value = value && value.nodeValue ? value.nodeValue : value; 3850 } 3851 3852 if (!value) { 3853 value = elm.getAttribute(name, 2); 3854 } 3855 3856 // Check boolean attribs 3857 if (/^(checked|compact|declare|defer|disabled|ismap|multiple|nohref|noshade|nowrap|readonly|selected)$/.test(name)) { 3858 if (elm[self.props[name]] === true && value === '') { 3859 return name; 3860 } 3861 3862 return value ? name : ''; 3863 } 3864 3865 // Inner input elements will override attributes on form elements 3866 if (elm.nodeName === "FORM" && elm.getAttributeNode(name)) { 3867 return elm.getAttributeNode(name).nodeValue; 3868 } 3869 3870 if (name === 'style') { 3871 value = value || elm.style.cssText; 3872 3873 if (value) { 3874 value = self.serializeStyle(self.parseStyle(value), elm.nodeName); 3875 3876 if (self.settings.keep_values) { 3877 elm.setAttribute('data-mce-style', value); 3878 } 3879 } 3880 } 3881 3882 // Remove Apple and WebKit stuff 3883 if (isWebKit && name === "class" && value) { 3884 value = value.replace(/(apple|webkit)\-[a-z\-]+/gi, ''); 3885 } 3886 3887 // Handle IE issues 3888 if (isIE) { 3889 switch (name) { 3890 case 'rowspan': 3891 case 'colspan': 3892 // IE returns 1 as default value 3893 if (value === 1) { 3894 value = ''; 3895 } 3896 3897 break; 3898 3899 case 'size': 3900 // IE returns +0 as default value for size 3901 if (value === '+0' || value === 20 || value === 0) { 3902 value = ''; 3903 } 3904 3905 break; 3906 3907 case 'width': 3908 case 'height': 3909 case 'vspace': 3910 case 'checked': 3911 case 'disabled': 3912 case 'readonly': 3913 if (value === 0) { 3914 value = ''; 3915 } 3916 3917 break; 3918 3919 case 'hspace': 3920 // IE returns -1 as default value 3921 if (value === -1) { 3922 value = ''; 3923 } 3924 3925 break; 3926 3927 case 'maxlength': 3928 case 'tabindex': 3929 // IE returns default value 3930 if (value === 32768 || value === 2147483647 || value === '32768') { 3931 value = ''; 3932 } 3933 3934 break; 3935 3936 case 'multiple': 3937 case 'compact': 3938 case 'noshade': 3939 case 'nowrap': 3940 if (value === 65535) { 3941 return name; 3942 } 3943 3944 return defaultVal; 3945 3946 case 'shape': 3947 value = value.toLowerCase(); 3948 break; 3949 3950 default: 3951 // IE has odd anonymous function for event attributes 3952 if (name.indexOf('on') === 0 && value) { 3953 value = ('' + value).replace(/^function\s+\w+\(\)\s+\{\s+(.*)\s+\}$/, '$1'); 3954 } 3955 } 3956 } 3957 3958 return (value !== undef && value !== null && value !== '') ? '' + value : defaultVal; 3959 }, 3960 3961 /** 3962 * Returns the absolute x, y position of a node. The position will be returned in an object with x, y fields. 3963 * 3964 * @method getPos 3965 * @param {Element/String} elm HTML element or element id to get x, y position from. 3966 * @param {Element} rootElm Optional root element to stop calculations at. 3967 * @return {object} Absolute position of the specified element object with x, y fields. 3968 */ 3969 getPos: function(elm, rootElm) { 3970 var self = this, x = 0, y = 0, offsetParent, doc = self.doc, pos; 3971 3972 elm = self.get(elm); 3973 rootElm = rootElm || doc.body; 3974 3975 if (elm) { 3976 // Use getBoundingClientRect if it exists since it's faster than looping offset nodes 3977 if (rootElm === doc.body && elm.getBoundingClientRect) { 3978 pos = elm.getBoundingClientRect(); 3979 rootElm = self.boxModel ? doc.documentElement : doc.body; 3980 3981 // Add scroll offsets from documentElement or body since IE with the wrong box model will use d.body and so do WebKit 3982 // Also remove the body/documentelement clientTop/clientLeft on IE 6, 7 since they offset the position 3983 x = pos.left + (doc.documentElement.scrollLeft || doc.body.scrollLeft) - rootElm.clientLeft; 3984 y = pos.top + (doc.documentElement.scrollTop || doc.body.scrollTop) - rootElm.clientTop; 3985 3986 return {x: x, y: y}; 3987 } 3988 3989 offsetParent = elm; 3990 while (offsetParent && offsetParent != rootElm && offsetParent.nodeType) { 3991 x += offsetParent.offsetLeft || 0; 3992 y += offsetParent.offsetTop || 0; 3993 offsetParent = offsetParent.offsetParent; 3994 } 3995 3996 offsetParent = elm.parentNode; 3997 while (offsetParent && offsetParent != rootElm && offsetParent.nodeType) { 3998 x -= offsetParent.scrollLeft || 0; 3999 y -= offsetParent.scrollTop || 0; 4000 offsetParent = offsetParent.parentNode; 4001 } 4002 } 4003 4004 return {x: x, y: y}; 4005 }, 4006 4007 /** 4008 * Parses the specified style value into an object collection. This parser will also 4009 * merge and remove any redundant items that browsers might have added. It will also convert non-hex 4010 * colors to hex values. Urls inside the styles will also be converted to absolute/relative based on settings. 4011 * 4012 * @method parseStyle 4013 * @param {String} cssText Style value to parse, for example: border:1px solid red;. 4014 * @return {Object} Object representation of that style, for example: {border: '1px solid red'} 4015 */ 4016 parseStyle: function(cssText) { 4017 return this.styles.parse(cssText); 4018 }, 4019 4020 /** 4021 * Serializes the specified style object into a string. 4022 * 4023 * @method serializeStyle 4024 * @param {Object} styles Object to serialize as string, for example: {border: '1px solid red'} 4025 * @param {String} name Optional element name. 4026 * @return {String} String representation of the style object, for example: border: 1px solid red. 4027 */ 4028 serializeStyle: function(styles, name) { 4029 return this.styles.serialize(styles, name); 4030 }, 4031 4032 /** 4033 * Adds a style element at the top of the document with the specified cssText content. 4034 * 4035 * @method addStyle 4036 * @param {String} cssText CSS Text style to add to top of head of document. 4037 */ 4038 addStyle: function(cssText) { 4039 var self = this, doc = self.doc, head, styleElm; 4040 4041 // Prevent inline from loading the same styles twice 4042 if (self !== DOMUtils.DOM && doc === document) { 4043 var addedStyles = DOMUtils.DOM.addedStyles; 4044 4045 addedStyles = addedStyles || []; 4046 if (addedStyles[cssText]) { 4047 return; 4048 } 4049 4050 addedStyles[cssText] = true; 4051 DOMUtils.DOM.addedStyles = addedStyles; 4052 } 4053 4054 // Create style element if needed 4055 styleElm = doc.getElementById('mceDefaultStyles'); 4056 if (!styleElm) { 4057 styleElm = doc.createElement('style'); 4058 styleElm.id = 'mceDefaultStyles'; 4059 styleElm.type = 'text/css'; 4060 4061 head = doc.getElementsByTagName('head')[0]; 4062 if (head.firstChild) { 4063 head.insertBefore(styleElm, head.firstChild); 4064 } else { 4065 head.appendChild(styleElm); 4066 } 4067 } 4068 4069 // Append style data to old or new style element 4070 if (styleElm.styleSheet) { 4071 styleElm.styleSheet.cssText += cssText; 4072 } else { 4073 styleElm.appendChild(doc.createTextNode(cssText)); 4074 } 4075 }, 4076 4077 /** 4078 * Imports/loads the specified CSS file into the document bound to the class. 4079 * 4080 * @method loadCSS 4081 * @param {String} u URL to CSS file to load. 4082 * @example 4083 * // Loads a CSS file dynamically into the current document 4084 * tinymce.DOM.loadCSS('somepath/some.css'); 4085 * 4086 * // Loads a CSS file into the currently active editor instance 4087 * tinymce.activeEditor.dom.loadCSS('somepath/some.css'); 4088 * 4089 * // Loads a CSS file into an editor instance by id 4090 * tinymce.get('someid').dom.loadCSS('somepath/some.css'); 4091 * 4092 * // Loads multiple CSS files into the current document 4093 * tinymce.DOM.loadCSS('somepath/some.css,somepath/someother.css'); 4094 */ 4095 loadCSS: function(url) { 4096 var self = this, doc = self.doc, head; 4097 4098 // Prevent inline from loading the same CSS file twice 4099 if (self !== DOMUtils.DOM && doc === document) { 4100 DOMUtils.DOM.loadCSS(url); 4101 return; 4102 } 4103 4104 if (!url) { 4105 url = ''; 4106 } 4107 4108 head = doc.getElementsByTagName('head')[0]; 4109 4110 each(url.split(','), function(url) { 4111 var link; 4112 4113 if (self.files[url]) { 4114 return; 4115 } 4116 4117 self.files[url] = true; 4118 link = self.create('link', {rel: 'stylesheet', href: url}); 4119 4120 // IE 8 has a bug where dynamically loading stylesheets would produce a 1 item remaining bug 4121 // This fix seems to resolve that issue by recalcing the document once a stylesheet finishes loading 4122 // It's ugly but it seems to work fine. 4123 if (isIE && doc.documentMode && doc.recalc) { 4124 link.onload = function() { 4125 if (doc.recalc) { 4126 doc.recalc(); 4127 } 4128 4129 link.onload = null; 4130 }; 4131 } 4132 4133 head.appendChild(link); 4134 }); 4135 }, 4136 4137 /** 4138 * Adds a class to the specified element or elements. 4139 * 4140 * @method addClass 4141 * @param {String/Element/Array} elm Element ID string or DOM element or array with elements or IDs. 4142 * @param {String} cls Class name to add to each element. 4143 * @return {String/Array} String with new class value or array with new class values for all elements. 4144 * @example 4145 * // Adds a class to all paragraphs in the active editor 4146 * tinymce.activeEditor.dom.addClass(tinymce.activeEditor.dom.select('p'), 'myclass'); 4147 * 4148 * // Adds a class to a specific element in the current page 4149 * tinymce.DOM.addClass('mydiv', 'myclass'); 4150 */ 4151 addClass: function(elm, cls) { 4152 return this.run(elm, function(elm) { 4153 var clsVal; 4154 4155 if (!cls) { 4156 return 0; 4157 } 4158 4159 if (this.hasClass(elm, cls)) { 4160 return elm.className; 4161 } 4162 4163 clsVal = this.removeClass(elm, cls); 4164 elm.className = clsVal = (clsVal !== '' ? (clsVal + ' ') : '') + cls; 4165 4166 return clsVal; 4167 }); 4168 }, 4169 4170 /** 4171 * Removes a class from the specified element or elements. 4172 * 4173 * @method removeClass 4174 * @param {String/Element/Array} elm Element ID string or DOM element or array with elements or IDs. 4175 * @param {String} cls Class name to remove from each element. 4176 * @return {String/Array} String of remaining class name(s), or an array of strings if multiple input elements 4177 * were passed in. 4178 * @example 4179 * // Removes a class from all paragraphs in the active editor 4180 * tinymce.activeEditor.dom.removeClass(tinymce.activeEditor.dom.select('p'), 'myclass'); 4181 * 4182 * // Removes a class from a specific element in the current page 4183 * tinymce.DOM.removeClass('mydiv', 'myclass'); 4184 */ 4185 removeClass: function(elm, cls) { 4186 var self = this, re; 4187 4188 return self.run(elm, function(elm) { 4189 var val; 4190 4191 if (self.hasClass(elm, cls)) { 4192 if (!re) { 4193 re = new RegExp("(^|\\s+)" + cls + "(\\s+|$)", "g"); 4194 } 4195 4196 val = elm.className.replace(re, ' '); 4197 val = trim(val != ' ' ? val : ''); 4198 4199 elm.className = val; 4200 4201 // Empty class attr 4202 if (!val) { 4203 elm.removeAttribute('class'); 4204 elm.removeAttribute('className'); 4205 } 4206 4207 return val; 4208 } 4209 4210 return elm.className; 4211 }); 4212 }, 4213 4214 /** 4215 * Returns true if the specified element has the specified class. 4216 * 4217 * @method hasClass 4218 * @param {String/Element} n HTML element or element id string to check CSS class on. 4219 * @param {String} c CSS class to check for. 4220 * @return {Boolean} true/false if the specified element has the specified class. 4221 */ 4222 hasClass: function(elm, cls) { 4223 elm = this.get(elm); 4224 4225 if (!elm || !cls) { 4226 return false; 4227 } 4228 4229 return (' ' + elm.className + ' ').indexOf(' ' + cls + ' ') !== -1; 4230 }, 4231 4232 /** 4233 * Toggles the specified class on/off. 4234 * 4235 * @method toggleClass 4236 * @param {Element} elm Element to toggle class on. 4237 * @param {[type]} cls Class to toggle on/off. 4238 * @param {[type]} state Optional state to set. 4239 */ 4240 toggleClass: function(elm, cls, state) { 4241 state = state === undefined ? !this.hasClass(elm, cls) : state; 4242 4243 if (this.hasClass(elm, cls) !== state) { 4244 if (state) { 4245 this.addClass(elm, cls); 4246 } else { 4247 this.removeClass(elm, cls); 4248 } 4249 } 4250 }, 4251 4252 /** 4253 * Shows the specified element(s) by ID by setting the "display" style. 4254 * 4255 * @method show 4256 * @param {String/Element/Array} elm ID of DOM element or DOM element or array with elements or IDs to show. 4257 */ 4258 show: function(elm) { 4259 return this.setStyle(elm, 'display', 'block'); 4260 }, 4261 4262 /** 4263 * Hides the specified element(s) by ID by setting the "display" style. 4264 * 4265 * @method hide 4266 * @param {String/Element/Array} e ID of DOM element or DOM element or array with elements or IDs to hide. 4267 * @example 4268 * // Hides an element by id in the document 4269 * tinymce.DOM.hide('myid'); 4270 */ 4271 hide: function(elm) { 4272 return this.setStyle(elm, 'display', 'none'); 4273 }, 4274 4275 /** 4276 * Returns true/false if the element is hidden or not by checking the "display" style. 4277 * 4278 * @method isHidden 4279 * @param {String/Element} e Id or element to check display state on. 4280 * @return {Boolean} true/false if the element is hidden or not. 4281 */ 4282 isHidden: function(elm) { 4283 elm = this.get(elm); 4284 4285 return !elm || elm.style.display == 'none' || this.getStyle(elm, 'display') == 'none'; 4286 }, 4287 4288 /** 4289 * Returns a unique id. This can be useful when generating elements on the fly. 4290 * This method will not check if the element already exists. 4291 * 4292 * @method uniqueId 4293 * @param {String} prefix Optional prefix to add in front of all ids - defaults to "mce_". 4294 * @return {String} Unique id. 4295 */ 4296 uniqueId: function(prefix) { 4297 return (!prefix ? 'mce_' : prefix) + (this.counter++); 4298 }, 4299 4300 /** 4301 * Sets the specified HTML content inside the element or elements. The HTML will first be processed. This means 4302 * URLs will get converted, hex color values fixed etc. Check processHTML for details. 4303 * 4304 * @method setHTML 4305 * @param {Element/String/Array} e DOM element, element id string or array of elements/ids to set HTML inside of. 4306 * @param {String} h HTML content to set as inner HTML of the element. 4307 * @example 4308 * // Sets the inner HTML of all paragraphs in the active editor 4309 * tinymce.activeEditor.dom.setHTML(tinymce.activeEditor.dom.select('p'), 'some inner html'); 4310 * 4311 * // Sets the inner HTML of an element by id in the document 4312 * tinymce.DOM.setHTML('mydiv', 'some inner html'); 4313 */ 4314 setHTML: function(element, html) { 4315 var self = this; 4316 4317 return self.run(element, function(element) { 4318 if (isIE) { 4319 // Remove all child nodes, IE keeps empty text nodes in DOM 4320 while (element.firstChild) { 4321 element.removeChild(element.firstChild); 4322 } 4323 4324 try { 4325 // IE will remove comments from the beginning 4326 // unless you padd the contents with something 4327 element.innerHTML = '<br />' + html; 4328 element.removeChild(element.firstChild); 4329 } catch (ex) { 4330 // IE sometimes produces an unknown runtime error on innerHTML if it's a block element 4331 // within a block element for example a div inside a p 4332 // This seems to fix this problem 4333 4334 // Create new div with HTML contents and a BR in front to keep comments 4335 var newElement = self.create('div'); 4336 newElement.innerHTML = '<br />' + html; 4337 4338 // Add all children from div to target 4339 each(grep(newElement.childNodes), function(node, i) { 4340 // Skip br element 4341 if (i && element.canHaveHTML) { 4342 element.appendChild(node); 4343 } 4344 }); 4345 } 4346 } else { 4347 element.innerHTML = html; 4348 } 4349 4350 return html; 4351 }); 4352 }, 4353 4354 /** 4355 * Returns the outer HTML of an element. 4356 * 4357 * @method getOuterHTML 4358 * @param {String/Element} elm Element ID or element object to get outer HTML from. 4359 * @return {String} Outer HTML string. 4360 * @example 4361 * tinymce.DOM.getOuterHTML(editorElement); 4362 * tinymce.activeEditor.getOuterHTML(tinymce.activeEditor.getBody()); 4363 */ 4364 getOuterHTML: function(elm) { 4365 var doc, self = this; 4366 4367 elm = self.get(elm); 4368 4369 if (!elm) { 4370 return null; 4371 } 4372 4373 if (elm.nodeType === 1 && self.hasOuterHTML) { 4374 return elm.outerHTML; 4375 } 4376 4377 doc = (elm.ownerDocument || self.doc).createElement("body"); 4378 doc.appendChild(elm.cloneNode(true)); 4379 4380 return doc.innerHTML; 4381 }, 4382 4383 /** 4384 * Sets the specified outer HTML on an element or elements. 4385 * 4386 * @method setOuterHTML 4387 * @param {Element/String/Array} elm DOM element, element id string or array of elements/ids to set outer HTML on. 4388 * @param {Object} html HTML code to set as outer value for the element. 4389 * @param {Document} doc Optional document scope to use in this process - defaults to the document of the DOM class. 4390 * @example 4391 * // Sets the outer HTML of all paragraphs in the active editor 4392 * tinymce.activeEditor.dom.setOuterHTML(tinymce.activeEditor.dom.select('p'), '<div>some html</div>'); 4393 * 4394 * // Sets the outer HTML of an element by id in the document 4395 * tinymce.DOM.setOuterHTML('mydiv', '<div>some html</div>'); 4396 */ 4397 setOuterHTML: function(elm, html, doc) { 4398 var self = this; 4399 4400 return self.run(elm, function(elm) { 4401 function set() { 4402 var node, tempElm; 4403 4404 tempElm = doc.createElement("body"); 4405 tempElm.innerHTML = html; 4406 4407 node = tempElm.lastChild; 4408 while (node) { 4409 self.insertAfter(node.cloneNode(true), elm); 4410 node = node.previousSibling; 4411 } 4412 4413 self.remove(elm); 4414 } 4415 4416 // Only set HTML on elements 4417 if (elm.nodeType == 1) { 4418 doc = doc || elm.ownerDocument || self.doc; 4419 4420 if (isIE) { 4421 try { 4422 // Try outerHTML for IE it sometimes produces an unknown runtime error 4423 if (elm.nodeType == 1 && self.hasOuterHTML) { 4424 elm.outerHTML = html; 4425 } else { 4426 set(); 4427 } 4428 } catch (ex) { 4429 // Fix for unknown runtime error 4430 set(); 4431 } 4432 } else { 4433 set(); 4434 } 4435 } 4436 }); 4437 }, 4438 4439 /** 4440 * Entity decodes a string. This method decodes any HTML entities, such as å. 4441 * 4442 * @method decode 4443 * @param {String} s String to decode entities on. 4444 * @return {String} Entity decoded string. 4445 */ 4446 decode: Entities.decode, 4447 4448 /** 4449 * Entity encodes a string. This method encodes the most common entities, such as <>"&. 4450 * 4451 * @method encode 4452 * @param {String} text String to encode with entities. 4453 * @return {String} Entity encoded string. 4454 */ 4455 encode: Entities.encodeAllRaw, 4456 4457 /** 4458 * Inserts an element after the reference element. 4459 * 4460 * @method insertAfter 4461 * @param {Element} node Element to insert after the reference. 4462 * @param {Element/String/Array} reference_node Reference element, element id or array of elements to insert after. 4463 * @return {Element/Array} Element that got added or an array with elements. 4464 */ 4465 insertAfter: function(node, reference_node) { 4466 reference_node = this.get(reference_node); 4467 4468 return this.run(node, function(node) { 4469 var parent, nextSibling; 4470 4471 parent = reference_node.parentNode; 4472 nextSibling = reference_node.nextSibling; 4473 4474 if (nextSibling) { 4475 parent.insertBefore(node, nextSibling); 4476 } else { 4477 parent.appendChild(node); 4478 } 4479 4480 return node; 4481 }); 4482 }, 4483 4484 /** 4485 * Replaces the specified element or elements with the new element specified. The new element will 4486 * be cloned if multiple input elements are passed in. 4487 * 4488 * @method replace 4489 * @param {Element} newElm New element to replace old ones with. 4490 * @param {Element/String/Array} oldELm Element DOM node, element id or array of elements or ids to replace. 4491 * @param {Boolean} k Optional keep children state, if set to true child nodes from the old object will be added to new ones. 4492 */ 4493 replace: function(newElm, oldElm, keepChildren) { 4494 var self = this; 4495 4496 return self.run(oldElm, function(oldElm) { 4497 if (is(oldElm, 'array')) { 4498 newElm = newElm.cloneNode(true); 4499 } 4500 4501 if (keepChildren) { 4502 each(grep(oldElm.childNodes), function(node) { 4503 newElm.appendChild(node); 4504 }); 4505 } 4506 4507 return oldElm.parentNode.replaceChild(newElm, oldElm); 4508 }); 4509 }, 4510 4511 /** 4512 * Renames the specified element and keeps its attributes and children. 4513 * 4514 * @method rename 4515 * @param {Element} elm Element to rename. 4516 * @param {String} name Name of the new element. 4517 * @return {Element} New element or the old element if it needed renaming. 4518 */ 4519 rename: function(elm, name) { 4520 var self = this, newElm; 4521 4522 if (elm.nodeName != name.toUpperCase()) { 4523 // Rename block element 4524 newElm = self.create(name); 4525 4526 // Copy attribs to new block 4527 each(self.getAttribs(elm), function(attr_node) { 4528 self.setAttrib(newElm, attr_node.nodeName, self.getAttrib(elm, attr_node.nodeName)); 4529 }); 4530 4531 // Replace block 4532 self.replace(newElm, elm, 1); 4533 } 4534 4535 return newElm || elm; 4536 }, 4537 4538 /** 4539 * Find the common ancestor of two elements. This is a shorter method than using the DOM Range logic. 4540 * 4541 * @method findCommonAncestor 4542 * @param {Element} a Element to find common ancestor of. 4543 * @param {Element} b Element to find common ancestor of. 4544 * @return {Element} Common ancestor element of the two input elements. 4545 */ 4546 findCommonAncestor: function(a, b) { 4547 var ps = a, pe; 4548 4549 while (ps) { 4550 pe = b; 4551 4552 while (pe && ps != pe) { 4553 pe = pe.parentNode; 4554 } 4555 4556 if (ps == pe) { 4557 break; 4558 } 4559 4560 ps = ps.parentNode; 4561 } 4562 4563 if (!ps && a.ownerDocument) { 4564 return a.ownerDocument.documentElement; 4565 } 4566 4567 return ps; 4568 }, 4569 4570 /** 4571 * Parses the specified RGB color value and returns a hex version of that color. 4572 * 4573 * @method toHex 4574 * @param {String} rgbVal RGB string value like rgb(1,2,3) 4575 * @return {String} Hex version of that RGB value like #FF00FF. 4576 */ 4577 toHex: function(rgbVal) { 4578 return this.styles.toHex(Tools.trim(rgbVal)); 4579 }, 4580 4581 /** 4582 * Executes the specified function on the element by id or dom element node or array of elements/id. 4583 * 4584 * @method run 4585 * @param {String/Element/Array} Element ID or DOM element object or array with ids or elements. 4586 * @param {function} f Function to execute for each item. 4587 * @param {Object} s Optional scope to execute the function in. 4588 * @return {Object/Array} Single object, or an array of objects if multiple input elements were passed in. 4589 */ 4590 run: function(elm, func, scope) { 4591 var self = this, result; 4592 4593 if (typeof(elm) === 'string') { 4594 elm = self.get(elm); 4595 } 4596 4597 if (!elm) { 4598 return false; 4599 } 4600 4601 scope = scope || this; 4602 if (!elm.nodeType && (elm.length || elm.length === 0)) { 4603 result = []; 4604 4605 each(elm, function(elm, i) { 4606 if (elm) { 4607 if (typeof(elm) == 'string') { 4608 elm = self.get(elm); 4609 } 4610 4611 result.push(func.call(scope, elm, i)); 4612 } 4613 }); 4614 4615 return result; 4616 } 4617 4618 return func.call(scope, elm); 4619 }, 4620 4621 /** 4622 * Returns a NodeList with attributes for the element. 4623 * 4624 * @method getAttribs 4625 * @param {HTMLElement/string} elm Element node or string id to get attributes from. 4626 * @return {NodeList} NodeList with attributes. 4627 */ 4628 getAttribs: function(elm) { 4629 var attrs; 4630 4631 elm = this.get(elm); 4632 4633 if (!elm) { 4634 return []; 4635 } 4636 4637 if (isIE) { 4638 attrs = []; 4639 4640 // Object will throw exception in IE 4641 if (elm.nodeName == 'OBJECT') { 4642 return elm.attributes; 4643 } 4644 4645 // IE doesn't keep the selected attribute if you clone option elements 4646 if (elm.nodeName === 'OPTION' && this.getAttrib(elm, 'selected')) { 4647 attrs.push({specified: 1, nodeName: 'selected'}); 4648 } 4649 4650 // It's crazy that this is faster in IE but it's because it returns all attributes all the time 4651 var attrRegExp = /<\/?[\w:\-]+ ?|=[\"][^\"]+\"|=\'[^\']+\'|=[\w\-]+|>/gi; 4652 elm.cloneNode(false).outerHTML.replace(attrRegExp, '').replace(/[\w:\-]+/gi, function(a) { 4653 attrs.push({specified: 1, nodeName: a}); 4654 }); 4655 4656 return attrs; 4657 } 4658 4659 return elm.attributes; 4660 }, 4661 4662 /** 4663 * Returns true/false if the specified node is to be considered empty or not. 4664 * 4665 * @example 4666 * tinymce.DOM.isEmpty(node, {img: true}); 4667 * @method isEmpty 4668 * @param {Object} elements Optional name/value object with elements that are automatically treated as non-empty elements. 4669 * @return {Boolean} true/false if the node is empty or not. 4670 */ 4671 isEmpty: function(node, elements) { 4672 var self = this, i, attributes, type, walker, name, brCount = 0; 4673 4674 node = node.firstChild; 4675 if (node) { 4676 walker = new TreeWalker(node, node.parentNode); 4677 elements = elements || self.schema ? self.schema.getNonEmptyElements() : null; 4678 4679 do { 4680 type = node.nodeType; 4681 4682 if (type === 1) { 4683 // Ignore bogus elements 4684 if (node.getAttribute('data-mce-bogus')) { 4685 continue; 4686 } 4687 4688 // Keep empty elements like <img /> 4689 name = node.nodeName.toLowerCase(); 4690 if (elements && elements[name]) { 4691 // Ignore single BR elements in blocks like <p><br /></p> or <p><span><br /></span></p> 4692 if (name === 'br') { 4693 brCount++; 4694 continue; 4695 } 4696 4697 return false; 4698 } 4699 4700 // Keep elements with data-bookmark attributes or name attribute like <a name="1"></a> 4701 attributes = self.getAttribs(node); 4702 i = attributes.length; 4703 while (i--) { 4704 name = attributes[i].nodeName; 4705 if (name === "name" || name === 'data-mce-bookmark') { 4706 return false; 4707 } 4708 } 4709 } 4710 4711 // Keep comment nodes 4712 if (type == 8) { 4713 return false; 4714 } 4715 4716 // Keep non whitespace text nodes 4717 if ((type === 3 && !whiteSpaceRegExp.test(node.nodeValue))) { 4718 return false; 4719 } 4720 } while ((node = walker.next())); 4721 } 4722 4723 return brCount <= 1; 4724 }, 4725 4726 /** 4727 * Creates a new DOM Range object. This will use the native DOM Range API if it's 4728 * available. If it's not, it will fall back to the custom TinyMCE implementation. 4729 * 4730 * @method createRng 4731 * @return {DOMRange} DOM Range object. 4732 * @example 4733 * var rng = tinymce.DOM.createRng(); 4734 * alert(rng.startContainer + "," + rng.startOffset); 4735 */ 4736 createRng: function() { 4737 var doc = this.doc; 4738 4739 return doc.createRange ? doc.createRange() : new Range(this); 4740 }, 4741 4742 /** 4743 * Returns the index of the specified node within its parent. 4744 * 4745 * @method nodeIndex 4746 * @param {Node} node Node to look for. 4747 * @param {boolean} normalized Optional true/false state if the index is what it would be after a normalization. 4748 * @return {Number} Index of the specified node. 4749 */ 4750 nodeIndex: function(node, normalized) { 4751 var idx = 0, lastNodeType, nodeType; 4752 4753 if (node) { 4754 for (lastNodeType = node.nodeType, node = node.previousSibling; node; node = node.previousSibling) { 4755 nodeType = node.nodeType; 4756 4757 // Normalize text nodes 4758 if (normalized && nodeType == 3) { 4759 if (nodeType == lastNodeType || !node.nodeValue.length) { 4760 continue; 4761 } 4762 } 4763 idx++; 4764 lastNodeType = nodeType; 4765 } 4766 } 4767 4768 return idx; 4769 }, 4770 4771 /** 4772 * Splits an element into two new elements and places the specified split 4773 * element or elements between the new ones. For example splitting the paragraph at the bold element in 4774 * this example <p>abc<b>abc</b>123</p> would produce <p>abc</p><b>abc</b><p>123</p>. 4775 * 4776 * @method split 4777 * @param {Element} parentElm Parent element to split. 4778 * @param {Element} splitElm Element to split at. 4779 * @param {Element} replacementElm Optional replacement element to replace the split element with. 4780 * @return {Element} Returns the split element or the replacement element if that is specified. 4781 */ 4782 split: function(parentElm, splitElm, replacementElm) { 4783 var self = this, r = self.createRng(), bef, aft, pa; 4784 4785 // W3C valid browsers tend to leave empty nodes to the left/right side of the contents - this makes sense 4786 // but we don't want that in our code since it serves no purpose for the end user 4787 // For example splitting this html at the bold element: 4788 // <p>text 1<span><b>CHOP</b></span>text 2</p> 4789 // would produce: 4790 // <p>text 1<span></span></p><b>CHOP</b><p><span></span>text 2</p> 4791 // this function will then trim off empty edges and produce: 4792 // <p>text 1</p><b>CHOP</b><p>text 2</p> 4793 function trimNode(node) { 4794 var i, children = node.childNodes, type = node.nodeType; 4795 4796 function surroundedBySpans(node) { 4797 var previousIsSpan = node.previousSibling && node.previousSibling.nodeName == 'SPAN'; 4798 var nextIsSpan = node.nextSibling && node.nextSibling.nodeName == 'SPAN'; 4799 return previousIsSpan && nextIsSpan; 4800 } 4801 4802 if (type == 1 && node.getAttribute('data-mce-type') == 'bookmark') { 4803 return; 4804 } 4805 4806 for (i = children.length - 1; i >= 0; i--) { 4807 trimNode(children[i]); 4808 } 4809 4810 if (type != 9) { 4811 // Keep non whitespace text nodes 4812 if (type == 3 && node.nodeValue.length > 0) { 4813 // If parent element isn't a block or there isn't any useful contents for example "<p> </p>" 4814 // Also keep text nodes with only spaces if surrounded by spans. 4815 // eg. "<p><span>a</span> <span>b</span></p>" should keep space between a and b 4816 var trimmedLength = trim(node.nodeValue).length; 4817 if (!self.isBlock(node.parentNode) || trimmedLength > 0 || trimmedLength === 0 && surroundedBySpans(node)) { 4818 return; 4819 } 4820 } else if (type == 1) { 4821 // If the only child is a bookmark then move it up 4822 children = node.childNodes; 4823 4824 // TODO fix this complex if 4825 if (children.length == 1 && children[0] && children[0].nodeType == 1 && 4826 children[0].getAttribute('data-mce-type') == 'bookmark') { 4827 node.parentNode.insertBefore(children[0], node); 4828 } 4829 4830 // Keep non empty elements or img, hr etc 4831 if (children.length || /^(br|hr|input|img)$/i.test(node.nodeName)) { 4832 return; 4833 } 4834 } 4835 4836 self.remove(node); 4837 } 4838 4839 return node; 4840 } 4841 4842 if (parentElm && splitElm) { 4843 // Get before chunk 4844 r.setStart(parentElm.parentNode, self.nodeIndex(parentElm)); 4845 r.setEnd(splitElm.parentNode, self.nodeIndex(splitElm)); 4846 bef = r.extractContents(); 4847 4848 // Get after chunk 4849 r = self.createRng(); 4850 r.setStart(splitElm.parentNode, self.nodeIndex(splitElm) + 1); 4851 r.setEnd(parentElm.parentNode, self.nodeIndex(parentElm) + 1); 4852 aft = r.extractContents(); 4853 4854 // Insert before chunk 4855 pa = parentElm.parentNode; 4856 pa.insertBefore(trimNode(bef), parentElm); 4857 4858 // Insert middle chunk 4859 if (replacementElm) { 4860 pa.replaceChild(replacementElm, splitElm); 4861 } else { 4862 pa.insertBefore(splitElm, parentElm); 4863 } 4864 4865 // Insert after chunk 4866 pa.insertBefore(trimNode(aft), parentElm); 4867 self.remove(parentElm); 4868 4869 return replacementElm || splitElm; 4870 } 4871 }, 4872 4873 /** 4874 * Adds an event handler to the specified object. 4875 * 4876 * @method bind 4877 * @param {Element/Document/Window/Array} target Target element to bind events to. 4878 * handler to or an array of elements/ids/documents. 4879 * @param {String} name Name of event handler to add, for example: click. 4880 * @param {function} func Function to execute when the event occurs. 4881 * @param {Object} scope Optional scope to execute the function in. 4882 * @return {function} Function callback handler the same as the one passed in. 4883 */ 4884 bind: function(target, name, func, scope) { 4885 var self = this; 4886 4887 if (Tools.isArray(target)) { 4888 var i = target.length; 4889 4890 while (i--) { 4891 target[i] = self.bind(target[i], name, func, scope); 4892 } 4893 4894 return target; 4895 } 4896 4897 // Collect all window/document events bound by editor instance 4898 if (self.settings.collect && (target === self.doc || target === self.win)) { 4899 self.boundEvents.push([target, name, func, scope]); 4900 } 4901 4902 return self.events.bind(target, name, func, scope || self); 4903 }, 4904 4905 /** 4906 * Removes the specified event handler by name and function from an element or collection of elements. 4907 * 4908 * @method unbind 4909 * @param {Element/Document/Window/Array} target Target element to unbind events on. 4910 * @param {String} name Event handler name, for example: "click" 4911 * @param {function} func Function to remove. 4912 * @return {bool/Array} Bool state of true if the handler was removed, or an array of states if multiple input elements 4913 * were passed in. 4914 */ 4915 unbind: function(target, name, func) { 4916 var self = this, i; 4917 4918 if (Tools.isArray(target)) { 4919 i = target.length; 4920 4921 while (i--) { 4922 target[i] = self.unbind(target[i], name, func); 4923 } 4924 4925 return target; 4926 } 4927 4928 // Remove any bound events matching the input 4929 if (self.boundEvents && (target === self.doc || target === self.win)) { 4930 i = self.boundEvents.length; 4931 4932 while (i--) { 4933 var item = self.boundEvents[i]; 4934 4935 if (target == item[0] && (!name || name == item[1]) && (!func || func == item[2])) { 4936 this.events.unbind(item[0], item[1], item[2]); 4937 } 4938 } 4939 } 4940 4941 return this.events.unbind(target, name, func); 4942 }, 4943 4944 /** 4945 * Fires the specified event name with object on target. 4946 * 4947 * @method fire 4948 * @param {Node/Document/Window} target Target element or object to fire event on. 4949 * @param {String} name Name of the event to fire. 4950 * @param {Object} evt Event object to send. 4951 * @return {Event} Event object. 4952 */ 4953 fire: function(target, name, evt) { 4954 return this.events.fire(target, name, evt); 4955 }, 4956 4957 // Returns the content editable state of a node 4958 getContentEditable: function(node) { 4959 var contentEditable; 4960 4961 // Check type 4962 if (!node || node.nodeType != 1) { 4963 return null; 4964 } 4965 4966 // Check for fake content editable 4967 contentEditable = node.getAttribute("data-mce-contenteditable"); 4968 if (contentEditable && contentEditable !== "inherit") { 4969 return contentEditable; 4970 } 4971 4972 // Check for real content editable 4973 return node.contentEditable !== "inherit" ? node.contentEditable : null; 4974 }, 4975 4976 getContentEditableParent: function(node) { 4977 var root = this.getRoot(), state = null; 4978 4979 for (; node && node !== root; node = node.parentNode) { 4980 state = this.getContentEditable(node); 4981 4982 if (state !== null) { 4983 break; 4984 } 4985 } 4986 4987 return state; 4988 }, 4989 4990 /** 4991 * Destroys all internal references to the DOM to solve IE leak issues. 4992 * 4993 * @method destroy 4994 */ 4995 destroy: function() { 4996 var self = this; 4997 4998 // Unbind all events bound to window/document by editor instance 4999 if (self.boundEvents) { 5000 var i = self.boundEvents.length; 5001 5002 while (i--) { 5003 var item = self.boundEvents[i]; 5004 this.events.unbind(item[0], item[1], item[2]); 5005 } 5006 5007 self.boundEvents = null; 5008 } 5009 5010 // Restore sizzle document to window.document 5011 // Since the current document might be removed producing "Permission denied" on IE see #6325 5012 if (Sizzle.setDocument) { 5013 Sizzle.setDocument(); 5014 } 5015 5016 self.win = self.doc = self.root = self.events = self.frag = null; 5017 }, 5018 5019 isChildOf: function(node, parent) { 5020 if (parent.contains) { 5021 return parent.contains(node); 5022 } 5023 5024 while (node) { 5025 if (parent === node) { 5026 return true; 5027 } 5028 5029 node = node.parentNode; 5030 } 5031 5032 return false; 5033 }, 5034 5035 // #ifdef debug 5036 5037 dumpRng: function(r) { 5038 return ( 5039 'startContainer: ' + r.startContainer.nodeName + 5040 ', startOffset: ' + r.startOffset + 5041 ', endContainer: ' + r.endContainer.nodeName + 5042 ', endOffset: ' + r.endOffset 5043 ); 5044 }, 5045 5046 // #endif 5047 5048 _findSib: function(node, selector, name) { 5049 var self = this, func = selector; 5050 5051 if (node) { 5052 // If expression make a function of it using is 5053 if (typeof(func) == 'string') { 5054 func = function(node) { 5055 return self.is(node, selector); 5056 }; 5057 } 5058 5059 // Loop all siblings 5060 for (node = node[name]; node; node = node[name]) { 5061 if (func(node)) { 5062 return node; 5063 } 5064 } 5065 } 5066 5067 return null; 5068 } 5069 }; 5070 5071 /** 5072 * Instance of DOMUtils for the current document. 5073 * 5074 * @static 5075 * @property DOM 5076 * @type tinymce.dom.DOMUtils 5077 * @example 5078 * // Example of how to add a class to some element by id 5079 * tinymce.DOM.addClass('someid', 'someclass'); 5080 */ 5081 DOMUtils.DOM = new DOMUtils(document); 5082 5083 return DOMUtils; 5084 }); 5085 5086 // Included from: js/tinymce/classes/dom/ScriptLoader.js 5087 5088 /** 5089 * ScriptLoader.js 5090 * 5091 * Copyright, Moxiecode Systems AB 5092 * Released under LGPL License. 5093 * 5094 * License: http://www.tinymce.com/license 5095 * Contributing: http://www.tinymce.com/contributing 5096 */ 5097 5098 /*globals console*/ 5099 5100 /** 5101 * This class handles asynchronous/synchronous loading of JavaScript files it will execute callbacks 5102 * when various items gets loaded. This class is useful to load external JavaScript files. 5103 * 5104 * @class tinymce.dom.ScriptLoader 5105 * @example 5106 * // Load a script from a specific URL using the global script loader 5107 * tinymce.ScriptLoader.load('somescript.js'); 5108 * 5109 * // Load a script using a unique instance of the script loader 5110 * var scriptLoader = new tinymce.dom.ScriptLoader(); 5111 * 5112 * scriptLoader.load('somescript.js'); 5113 * 5114 * // Load multiple scripts 5115 * var scriptLoader = new tinymce.dom.ScriptLoader(); 5116 * 5117 * scriptLoader.add('somescript1.js'); 5118 * scriptLoader.add('somescript2.js'); 5119 * scriptLoader.add('somescript3.js'); 5120 * 5121 * scriptLoader.loadQueue(function() { 5122 * alert('All scripts are now loaded.'); 5123 * }); 5124 */ 5125 define("tinymce/dom/ScriptLoader", [ 5126 "tinymce/dom/DOMUtils", 5127 "tinymce/util/Tools" 5128 ], function(DOMUtils, Tools) { 5129 var DOM = DOMUtils.DOM; 5130 var each = Tools.each, grep = Tools.grep; 5131 5132 function ScriptLoader() { 5133 var QUEUED = 0, 5134 LOADING = 1, 5135 LOADED = 2, 5136 states = {}, 5137 queue = [], 5138 scriptLoadedCallbacks = {}, 5139 queueLoadedCallbacks = [], 5140 loading = 0, 5141 undef; 5142 5143 /** 5144 * Loads a specific script directly without adding it to the load queue. 5145 * 5146 * @method load 5147 * @param {String} url Absolute URL to script to add. 5148 * @param {function} callback Optional callback function to execute ones this script gets loaded. 5149 * @param {Object} scope Optional scope to execute callback in. 5150 */ 5151 function loadScript(url, callback) { 5152 var dom = DOM, elm, id; 5153 5154 // Execute callback when script is loaded 5155 function done() { 5156 dom.remove(id); 5157 5158 if (elm) { 5159 elm.onreadystatechange = elm.onload = elm = null; 5160 } 5161 5162 callback(); 5163 } 5164 5165 function error() { 5166 /*eslint no-console:0 */ 5167 5168 // Report the error so it's easier for people to spot loading errors 5169 if (typeof(console) !== "undefined" && console.log) { 5170 console.log("Failed to load: " + url); 5171 } 5172 5173 // We can't mark it as done if there is a load error since 5174 // A) We don't want to produce 404 errors on the server and 5175 // B) the onerror event won't fire on all browsers. 5176 // done(); 5177 } 5178 5179 id = dom.uniqueId(); 5180 5181 // Create new script element 5182 elm = document.createElement('script'); 5183 elm.id = id; 5184 elm.type = 'text/javascript'; 5185 elm.src = url; 5186 5187 // Seems that onreadystatechange works better on IE 10 onload seems to fire incorrectly 5188 if ("onreadystatechange" in elm) { 5189 elm.onreadystatechange = function() { 5190 if (/loaded|complete/.test(elm.readyState)) { 5191 done(); 5192 } 5193 }; 5194 } else { 5195 elm.onload = done; 5196 } 5197 5198 // Add onerror event will get fired on some browsers but not all of them 5199 elm.onerror = error; 5200 5201 // Add script to document 5202 (document.getElementsByTagName('head')[0] || document.body).appendChild(elm); 5203 } 5204 5205 /** 5206 * Returns true/false if a script has been loaded or not. 5207 * 5208 * @method isDone 5209 * @param {String} url URL to check for. 5210 * @return {Boolean} true/false if the URL is loaded. 5211 */ 5212 this.isDone = function(url) { 5213 return states[url] == LOADED; 5214 }; 5215 5216 /** 5217 * Marks a specific script to be loaded. This can be useful if a script got loaded outside 5218 * the script loader or to skip it from loading some script. 5219 * 5220 * @method markDone 5221 * @param {string} u Absolute URL to the script to mark as loaded. 5222 */ 5223 this.markDone = function(url) { 5224 states[url] = LOADED; 5225 }; 5226 5227 /** 5228 * Adds a specific script to the load queue of the script loader. 5229 * 5230 * @method add 5231 * @param {String} url Absolute URL to script to add. 5232 * @param {function} callback Optional callback function to execute ones this script gets loaded. 5233 * @param {Object} scope Optional scope to execute callback in. 5234 */ 5235 this.add = this.load = function(url, callback, scope) { 5236 var state = states[url]; 5237 5238 // Add url to load queue 5239 if (state == undef) { 5240 queue.push(url); 5241 states[url] = QUEUED; 5242 } 5243 5244 if (callback) { 5245 // Store away callback for later execution 5246 if (!scriptLoadedCallbacks[url]) { 5247 scriptLoadedCallbacks[url] = []; 5248 } 5249 5250 scriptLoadedCallbacks[url].push({ 5251 func: callback, 5252 scope: scope || this 5253 }); 5254 } 5255 }; 5256 5257 /** 5258 * Starts the loading of the queue. 5259 * 5260 * @method loadQueue 5261 * @param {function} callback Optional callback to execute when all queued items are loaded. 5262 * @param {Object} scope Optional scope to execute the callback in. 5263 */ 5264 this.loadQueue = function(callback, scope) { 5265 this.loadScripts(queue, callback, scope); 5266 }; 5267 5268 /** 5269 * Loads the specified queue of files and executes the callback ones they are loaded. 5270 * This method is generally not used outside this class but it might be useful in some scenarios. 5271 * 5272 * @method loadScripts 5273 * @param {Array} scripts Array of queue items to load. 5274 * @param {function} callback Optional callback to execute ones all items are loaded. 5275 * @param {Object} scope Optional scope to execute callback in. 5276 */ 5277 this.loadScripts = function(scripts, callback, scope) { 5278 var loadScripts; 5279 5280 function execScriptLoadedCallbacks(url) { 5281 // Execute URL callback functions 5282 each(scriptLoadedCallbacks[url], function(callback) { 5283 callback.func.call(callback.scope); 5284 }); 5285 5286 scriptLoadedCallbacks[url] = undef; 5287 } 5288 5289 queueLoadedCallbacks.push({ 5290 func: callback, 5291 scope: scope || this 5292 }); 5293 5294 loadScripts = function() { 5295 var loadingScripts = grep(scripts); 5296 5297 // Current scripts has been handled 5298 scripts.length = 0; 5299 5300 // Load scripts that needs to be loaded 5301 each(loadingScripts, function(url) { 5302 // Script is already loaded then execute script callbacks directly 5303 if (states[url] == LOADED) { 5304 execScriptLoadedCallbacks(url); 5305 return; 5306 } 5307 5308 // Is script not loading then start loading it 5309 if (states[url] != LOADING) { 5310 states[url] = LOADING; 5311 loading++; 5312 5313 loadScript(url, function() { 5314 states[url] = LOADED; 5315 loading--; 5316 5317 execScriptLoadedCallbacks(url); 5318 5319 // Load more scripts if they where added by the recently loaded script 5320 loadScripts(); 5321 }); 5322 } 5323 }); 5324 5325 // No scripts are currently loading then execute all pending queue loaded callbacks 5326 if (!loading) { 5327 each(queueLoadedCallbacks, function(callback) { 5328 callback.func.call(callback.scope); 5329 }); 5330 5331 queueLoadedCallbacks.length = 0; 5332 } 5333 }; 5334 5335 loadScripts(); 5336 }; 5337 } 5338 5339 ScriptLoader.ScriptLoader = new ScriptLoader(); 5340 5341 return ScriptLoader; 5342 }); 5343 5344 // Included from: js/tinymce/classes/AddOnManager.js 5345 5346 /** 5347 * AddOnManager.js 5348 * 5349 * Copyright, Moxiecode Systems AB 5350 * Released under LGPL License. 5351 * 5352 * License: http://www.tinymce.com/license 5353 * Contributing: http://www.tinymce.com/contributing 5354 */ 5355 5356 /** 5357 * This class handles the loading of themes/plugins or other add-ons and their language packs. 5358 * 5359 * @class tinymce.AddOnManager 5360 */ 5361 define("tinymce/AddOnManager", [ 5362 "tinymce/dom/ScriptLoader", 5363 "tinymce/util/Tools" 5364 ], function(ScriptLoader, Tools) { 5365 var each = Tools.each; 5366 5367 function AddOnManager() { 5368 var self = this; 5369 5370 self.items = []; 5371 self.urls = {}; 5372 self.lookup = {}; 5373 } 5374 5375 AddOnManager.prototype = { 5376 /** 5377 * Returns the specified add on by the short name. 5378 * 5379 * @method get 5380 * @param {String} name Add-on to look for. 5381 * @return {tinymce.Theme/tinymce.Plugin} Theme or plugin add-on instance or undefined. 5382 */ 5383 get: function(name) { 5384 if (this.lookup[name]) { 5385 return this.lookup[name].instance; 5386 } else { 5387 return undefined; 5388 } 5389 }, 5390 5391 dependencies: function(name) { 5392 var result; 5393 5394 if (this.lookup[name]) { 5395 result = this.lookup[name].dependencies; 5396 } 5397 5398 return result || []; 5399 }, 5400 5401 /** 5402 * Loads a language pack for the specified add-on. 5403 * 5404 * @method requireLangPack 5405 * @param {String} name Short name of the add-on. 5406 * @param {String} languages Optional comma or space separated list of languages to check if it matches the name. 5407 */ 5408 requireLangPack: function(name, languages) { 5409 var language = AddOnManager.language; 5410 5411 if (language && AddOnManager.languageLoad !== false) { 5412 if (languages) { 5413 languages = ',' + languages + ','; 5414 5415 // Load short form sv.js or long form sv_SE.js 5416 if (languages.indexOf(',' + language.substr(0, 2) + ',') != -1) { 5417 language = language.substr(0, 2); 5418 } else if (languages.indexOf(',' + language + ',') == -1) { 5419 return; 5420 } 5421 } 5422 5423 ScriptLoader.ScriptLoader.add(this.urls[name] + '/langs/' + language + '.js'); 5424 } 5425 }, 5426 5427 /** 5428 * Adds a instance of the add-on by it's short name. 5429 * 5430 * @method add 5431 * @param {String} id Short name/id for the add-on. 5432 * @param {tinymce.Theme/tinymce.Plugin} addOn Theme or plugin to add. 5433 * @return {tinymce.Theme/tinymce.Plugin} The same theme or plugin instance that got passed in. 5434 * @example 5435 * // Create a simple plugin 5436 * tinymce.create('tinymce.plugins.TestPlugin', { 5437 * TestPlugin: function(ed, url) { 5438 * ed.on('click', function(e) { 5439 * ed.windowManager.alert('Hello World!'); 5440 * }); 5441 * } 5442 * }); 5443 * 5444 * // Register plugin using the add method 5445 * tinymce.PluginManager.add('test', tinymce.plugins.TestPlugin); 5446 * 5447 * // Initialize TinyMCE 5448 * tinymce.init({ 5449 * ... 5450 * plugins: '-test' // Init the plugin but don't try to load it 5451 * }); 5452 */ 5453 add: function(id, addOn, dependencies) { 5454 this.items.push(addOn); 5455 this.lookup[id] = {instance: addOn, dependencies: dependencies}; 5456 5457 return addOn; 5458 }, 5459 5460 createUrl: function(baseUrl, dep) { 5461 if (typeof dep === "object") { 5462 return dep; 5463 } else { 5464 return {prefix: baseUrl.prefix, resource: dep, suffix: baseUrl.suffix}; 5465 } 5466 }, 5467 5468 /** 5469 * Add a set of components that will make up the add-on. Using the url of the add-on name as the base url. 5470 * This should be used in development mode. A new compressor/javascript munger process will ensure that the 5471 * components are put together into the plugin.js file and compressed correctly. 5472 * 5473 * @method addComponents 5474 * @param {String} pluginName name of the plugin to load scripts from (will be used to get the base url for the plugins). 5475 * @param {Array} scripts Array containing the names of the scripts to load. 5476 */ 5477 addComponents: function(pluginName, scripts) { 5478 var pluginUrl = this.urls[pluginName]; 5479 5480 each(scripts, function(script) { 5481 ScriptLoader.ScriptLoader.add(pluginUrl + "/" + script); 5482 }); 5483 }, 5484 5485 /** 5486 * Loads an add-on from a specific url. 5487 * 5488 * @method load 5489 * @param {String} name Short name of the add-on that gets loaded. 5490 * @param {String} addOnUrl URL to the add-on that will get loaded. 5491 * @param {function} callback Optional callback to execute ones the add-on is loaded. 5492 * @param {Object} scope Optional scope to execute the callback in. 5493 * @example 5494 * // Loads a plugin from an external URL 5495 * tinymce.PluginManager.load('myplugin', '/some/dir/someplugin/plugin.js'); 5496 * 5497 * // Initialize TinyMCE 5498 * tinymce.init({ 5499 * ... 5500 * plugins: '-myplugin' // Don't try to load it again 5501 * }); 5502 */ 5503 load: function(name, addOnUrl, callback, scope) { 5504 var self = this, url = addOnUrl; 5505 5506 function loadDependencies() { 5507 var dependencies = self.dependencies(name); 5508 5509 each(dependencies, function(dep) { 5510 var newUrl = self.createUrl(addOnUrl, dep); 5511 5512 self.load(newUrl.resource, newUrl, undefined, undefined); 5513 }); 5514 5515 if (callback) { 5516 if (scope) { 5517 callback.call(scope); 5518 } else { 5519 callback.call(ScriptLoader); 5520 } 5521 } 5522 } 5523 5524 if (self.urls[name]) { 5525 return; 5526 } 5527 5528 if (typeof addOnUrl === "object") { 5529 url = addOnUrl.prefix + addOnUrl.resource + addOnUrl.suffix; 5530 } 5531 5532 if (url.indexOf('/') !== 0 && url.indexOf('://') == -1) { 5533 url = AddOnManager.baseURL + '/' + url; 5534 } 5535 5536 self.urls[name] = url.substring(0, url.lastIndexOf('/')); 5537 5538 if (self.lookup[name]) { 5539 loadDependencies(); 5540 } else { 5541 ScriptLoader.ScriptLoader.add(url, loadDependencies, scope); 5542 } 5543 } 5544 }; 5545 5546 AddOnManager.PluginManager = new AddOnManager(); 5547 AddOnManager.ThemeManager = new AddOnManager(); 5548 5549 return AddOnManager; 5550 }); 5551 5552 /** 5553 * TinyMCE theme class. 5554 * 5555 * @class tinymce.Theme 5556 */ 5557 5558 /** 5559 * This method is responsible for rendering/generating the overall user interface with toolbars, buttons, iframe containers etc. 5560 * 5561 * @method renderUI 5562 * @param {Object} obj Object parameter containing the targetNode DOM node that will be replaced visually with an editor instance. 5563 * @return {Object} an object with items like iframeContainer, editorContainer, sizeContainer, deltaWidth, deltaHeight. 5564 */ 5565 5566 /** 5567 * Plugin base class, this is a pseudo class that describes how a plugin is to be created for TinyMCE. The methods below are all optional. 5568 * 5569 * @class tinymce.Plugin 5570 * @example 5571 * tinymce.PluginManager.add('example', function(editor, url) { 5572 * // Add a button that opens a window 5573 * editor.addButton('example', { 5574 * text: 'My button', 5575 * icon: false, 5576 * onclick: function() { 5577 * // Open window 5578 * editor.windowManager.open({ 5579 * title: 'Example plugin', 5580 * body: [ 5581 * {type: 'textbox', name: 'title', label: 'Title'} 5582 * ], 5583 * onsubmit: function(e) { 5584 * // Insert content when the window form is submitted 5585 * editor.insertContent('Title: ' + e.data.title); 5586 * } 5587 * }); 5588 * } 5589 * }); 5590 * 5591 * // Adds a menu item to the tools menu 5592 * editor.addMenuItem('example', { 5593 * text: 'Example plugin', 5594 * context: 'tools', 5595 * onclick: function() { 5596 * // Open window with a specific url 5597 * editor.windowManager.open({ 5598 * title: 'TinyMCE site', 5599 * url: 'http://www.tinymce.com', 5600 * width: 800, 5601 * height: 600, 5602 * buttons: [{ 5603 * text: 'Close', 5604 * onclick: 'close' 5605 * }] 5606 * }); 5607 * } 5608 * }); 5609 * }); 5610 */ 5611 5612 // Included from: js/tinymce/classes/html/Node.js 5613 5614 /** 5615 * Node.js 5616 * 5617 * Copyright, Moxiecode Systems AB 5618 * Released under LGPL License. 5619 * 5620 * License: http://www.tinymce.com/license 5621 * Contributing: http://www.tinymce.com/contributing 5622 */ 5623 5624 /** 5625 * This class is a minimalistic implementation of a DOM like node used by the DomParser class. 5626 * 5627 * @example 5628 * var node = new tinymce.html.Node('strong', 1); 5629 * someRoot.append(node); 5630 * 5631 * @class tinymce.html.Node 5632 * @version 3.4 5633 */ 5634 define("tinymce/html/Node", [], function() { 5635 var whiteSpaceRegExp = /^[ \t\r\n]*$/, typeLookup = { 5636 '#text': 3, 5637 '#comment': 8, 5638 '#cdata': 4, 5639 '#pi': 7, 5640 '#doctype': 10, 5641 '#document-fragment': 11 5642 }; 5643 5644 // Walks the tree left/right 5645 function walk(node, root_node, prev) { 5646 var sibling, parent, startName = prev ? 'lastChild' : 'firstChild', siblingName = prev ? 'prev' : 'next'; 5647 5648 // Walk into nodes if it has a start 5649 if (node[startName]) { 5650 return node[startName]; 5651 } 5652 5653 // Return the sibling if it has one 5654 if (node !== root_node) { 5655 sibling = node[siblingName]; 5656 5657 if (sibling) { 5658 return sibling; 5659 } 5660 5661 // Walk up the parents to look for siblings 5662 for (parent = node.parent; parent && parent !== root_node; parent = parent.parent) { 5663 sibling = parent[siblingName]; 5664 5665 if (sibling) { 5666 return sibling; 5667 } 5668 } 5669 } 5670 } 5671 5672 /** 5673 * Constructs a new Node instance. 5674 * 5675 * @constructor 5676 * @method Node 5677 * @param {String} name Name of the node type. 5678 * @param {Number} type Numeric type representing the node. 5679 */ 5680 function Node(name, type) { 5681 this.name = name; 5682 this.type = type; 5683 5684 if (type === 1) { 5685 this.attributes = []; 5686 this.attributes.map = {}; 5687 } 5688 } 5689 5690 Node.prototype = { 5691 /** 5692 * Replaces the current node with the specified one. 5693 * 5694 * @example 5695 * someNode.replace(someNewNode); 5696 * 5697 * @method replace 5698 * @param {tinymce.html.Node} node Node to replace the current node with. 5699 * @return {tinymce.html.Node} The old node that got replaced. 5700 */ 5701 replace: function(node) { 5702 var self = this; 5703 5704 if (node.parent) { 5705 node.remove(); 5706 } 5707 5708 self.insert(node, self); 5709 self.remove(); 5710 5711 return self; 5712 }, 5713 5714 /** 5715 * Gets/sets or removes an attribute by name. 5716 * 5717 * @example 5718 * someNode.attr("name", "value"); // Sets an attribute 5719 * console.log(someNode.attr("name")); // Gets an attribute 5720 * someNode.attr("name", null); // Removes an attribute 5721 * 5722 * @method attr 5723 * @param {String} name Attribute name to set or get. 5724 * @param {String} value Optional value to set. 5725 * @return {String/tinymce.html.Node} String or undefined on a get operation or the current node on a set operation. 5726 */ 5727 attr: function(name, value) { 5728 var self = this, attrs, i, undef; 5729 5730 if (typeof name !== "string") { 5731 for (i in name) { 5732 self.attr(i, name[i]); 5733 } 5734 5735 return self; 5736 } 5737 5738 if ((attrs = self.attributes)) { 5739 if (value !== undef) { 5740 // Remove attribute 5741 if (value === null) { 5742 if (name in attrs.map) { 5743 delete attrs.map[name]; 5744 5745 i = attrs.length; 5746 while (i--) { 5747 if (attrs[i].name === name) { 5748 attrs = attrs.splice(i, 1); 5749 return self; 5750 } 5751 } 5752 } 5753 5754 return self; 5755 } 5756 5757 // Set attribute 5758 if (name in attrs.map) { 5759 // Set attribute 5760 i = attrs.length; 5761 while (i--) { 5762 if (attrs[i].name === name) { 5763 attrs[i].value = value; 5764 break; 5765 } 5766 } 5767 } else { 5768 attrs.push({name: name, value: value}); 5769 } 5770 5771 attrs.map[name] = value; 5772 5773 return self; 5774 } else { 5775 return attrs.map[name]; 5776 } 5777 } 5778 }, 5779 5780 /** 5781 * Does a shallow clones the node into a new node. It will also exclude id attributes since 5782 * there should only be one id per document. 5783 * 5784 * @example 5785 * var clonedNode = node.clone(); 5786 * 5787 * @method clone 5788 * @return {tinymce.html.Node} New copy of the original node. 5789 */ 5790 clone: function() { 5791 var self = this, clone = new Node(self.name, self.type), i, l, selfAttrs, selfAttr, cloneAttrs; 5792 5793 // Clone element attributes 5794 if ((selfAttrs = self.attributes)) { 5795 cloneAttrs = []; 5796 cloneAttrs.map = {}; 5797 5798 for (i = 0, l = selfAttrs.length; i < l; i++) { 5799 selfAttr = selfAttrs[i]; 5800 5801 // Clone everything except id 5802 if (selfAttr.name !== 'id') { 5803 cloneAttrs[cloneAttrs.length] = {name: selfAttr.name, value: selfAttr.value}; 5804 cloneAttrs.map[selfAttr.name] = selfAttr.value; 5805 } 5806 } 5807 5808 clone.attributes = cloneAttrs; 5809 } 5810 5811 clone.value = self.value; 5812 clone.shortEnded = self.shortEnded; 5813 5814 return clone; 5815 }, 5816 5817 /** 5818 * Wraps the node in in another node. 5819 * 5820 * @example 5821 * node.wrap(wrapperNode); 5822 * 5823 * @method wrap 5824 */ 5825 wrap: function(wrapper) { 5826 var self = this; 5827 5828 self.parent.insert(wrapper, self); 5829 wrapper.append(self); 5830 5831 return self; 5832 }, 5833 5834 /** 5835 * Unwraps the node in other words it removes the node but keeps the children. 5836 * 5837 * @example 5838 * node.unwrap(); 5839 * 5840 * @method unwrap 5841 */ 5842 unwrap: function() { 5843 var self = this, node, next; 5844 5845 for (node = self.firstChild; node; ) { 5846 next = node.next; 5847 self.insert(node, self, true); 5848 node = next; 5849 } 5850 5851 self.remove(); 5852 }, 5853 5854 /** 5855 * Removes the node from it's parent. 5856 * 5857 * @example 5858 * node.remove(); 5859 * 5860 * @method remove 5861 * @return {tinymce.html.Node} Current node that got removed. 5862 */ 5863 remove: function() { 5864 var self = this, parent = self.parent, next = self.next, prev = self.prev; 5865 5866 if (parent) { 5867 if (parent.firstChild === self) { 5868 parent.firstChild = next; 5869 5870 if (next) { 5871 next.prev = null; 5872 } 5873 } else { 5874 prev.next = next; 5875 } 5876 5877 if (parent.lastChild === self) { 5878 parent.lastChild = prev; 5879 5880 if (prev) { 5881 prev.next = null; 5882 } 5883 } else { 5884 next.prev = prev; 5885 } 5886 5887 self.parent = self.next = self.prev = null; 5888 } 5889 5890 return self; 5891 }, 5892 5893 /** 5894 * Appends a new node as a child of the current node. 5895 * 5896 * @example 5897 * node.append(someNode); 5898 * 5899 * @method append 5900 * @param {tinymce.html.Node} node Node to append as a child of the current one. 5901 * @return {tinymce.html.Node} The node that got appended. 5902 */ 5903 append: function(node) { 5904 var self = this, last; 5905 5906 if (node.parent) { 5907 node.remove(); 5908 } 5909 5910 last = self.lastChild; 5911 if (last) { 5912 last.next = node; 5913 node.prev = last; 5914 self.lastChild = node; 5915 } else { 5916 self.lastChild = self.firstChild = node; 5917 } 5918 5919 node.parent = self; 5920 5921 return node; 5922 }, 5923 5924 /** 5925 * Inserts a node at a specific position as a child of the current node. 5926 * 5927 * @example 5928 * parentNode.insert(newChildNode, oldChildNode); 5929 * 5930 * @method insert 5931 * @param {tinymce.html.Node} node Node to insert as a child of the current node. 5932 * @param {tinymce.html.Node} ref_node Reference node to set node before/after. 5933 * @param {Boolean} before Optional state to insert the node before the reference node. 5934 * @return {tinymce.html.Node} The node that got inserted. 5935 */ 5936 insert: function(node, ref_node, before) { 5937 var parent; 5938 5939 if (node.parent) { 5940 node.remove(); 5941 } 5942 5943 parent = ref_node.parent || this; 5944 5945 if (before) { 5946 if (ref_node === parent.firstChild) { 5947 parent.firstChild = node; 5948 } else { 5949 ref_node.prev.next = node; 5950 } 5951 5952 node.prev = ref_node.prev; 5953 node.next = ref_node; 5954 ref_node.prev = node; 5955 } else { 5956 if (ref_node === parent.lastChild) { 5957 parent.lastChild = node; 5958 } else { 5959 ref_node.next.prev = node; 5960 } 5961 5962 node.next = ref_node.next; 5963 node.prev = ref_node; 5964 ref_node.next = node; 5965 } 5966 5967 node.parent = parent; 5968 5969 return node; 5970 }, 5971 5972 /** 5973 * Get all children by name. 5974 * 5975 * @method getAll 5976 * @param {String} name Name of the child nodes to collect. 5977 * @return {Array} Array with child nodes matchin the specified name. 5978 */ 5979 getAll: function(name) { 5980 var self = this, node, collection = []; 5981 5982 for (node = self.firstChild; node; node = walk(node, self)) { 5983 if (node.name === name) { 5984 collection.push(node); 5985 } 5986 } 5987 5988 return collection; 5989 }, 5990 5991 /** 5992 * Removes all children of the current node. 5993 * 5994 * @method empty 5995 * @return {tinymce.html.Node} The current node that got cleared. 5996 */ 5997 empty: function() { 5998 var self = this, nodes, i, node; 5999 6000 // Remove all children 6001 if (self.firstChild) { 6002 nodes = []; 6003 6004 // Collect the children 6005 for (node = self.firstChild; node; node = walk(node, self)) { 6006 nodes.push(node); 6007 } 6008 6009 // Remove the children 6010 i = nodes.length; 6011 while (i--) { 6012 node = nodes[i]; 6013 node.parent = node.firstChild = node.lastChild = node.next = node.prev = null; 6014 } 6015 } 6016 6017 self.firstChild = self.lastChild = null; 6018 6019 return self; 6020 }, 6021 6022 /** 6023 * Returns true/false if the node is to be considered empty or not. 6024 * 6025 * @example 6026 * node.isEmpty({img: true}); 6027 * @method isEmpty 6028 * @param {Object} elements Name/value object with elements that are automatically treated as non empty elements. 6029 * @return {Boolean} true/false if the node is empty or not. 6030 */ 6031 isEmpty: function(elements) { 6032 var self = this, node = self.firstChild, i, name; 6033 6034 if (node) { 6035 do { 6036 if (node.type === 1) { 6037 // Ignore bogus elements 6038 if (node.attributes.map['data-mce-bogus']) { 6039 continue; 6040 } 6041 6042 // Keep empty elements like <img /> 6043 if (elements[node.name]) { 6044 return false; 6045 } 6046 6047 // Keep elements with data attributes or name attribute like <a name="1"></a> 6048 i = node.attributes.length; 6049 while (i--) { 6050 name = node.attributes[i].name; 6051 if (name === "name" || name.indexOf('data-mce-') === 0) { 6052 return false; 6053 } 6054 } 6055 } 6056 6057 // Keep comments 6058 if (node.type === 8) { 6059 return false; 6060 } 6061 6062 // Keep non whitespace text nodes 6063 if ((node.type === 3 && !whiteSpaceRegExp.test(node.value))) { 6064 return false; 6065 } 6066 } while ((node = walk(node, self))); 6067 } 6068 6069 return true; 6070 }, 6071 6072 /** 6073 * Walks to the next or previous node and returns that node or null if it wasn't found. 6074 * 6075 * @method walk 6076 * @param {Boolean} prev Optional previous node state defaults to false. 6077 * @return {tinymce.html.Node} Node that is next to or previous of the current node. 6078 */ 6079 walk: function(prev) { 6080 return walk(this, null, prev); 6081 } 6082 }; 6083 6084 /** 6085 * Creates a node of a specific type. 6086 * 6087 * @static 6088 * @method create 6089 * @param {String} name Name of the node type to create for example "b" or "#text". 6090 * @param {Object} attrs Name/value collection of attributes that will be applied to elements. 6091 */ 6092 Node.create = function(name, attrs) { 6093 var node, attrName; 6094 6095 // Create node 6096 node = new Node(name, typeLookup[name] || 1); 6097 6098 // Add attributes if needed 6099 if (attrs) { 6100 for (attrName in attrs) { 6101 node.attr(attrName, attrs[attrName]); 6102 } 6103 } 6104 6105 return node; 6106 }; 6107 6108 return Node; 6109 }); 6110 6111 // Included from: js/tinymce/classes/html/Schema.js 6112 6113 /** 6114 * Schema.js 6115 * 6116 * Copyright, Moxiecode Systems AB 6117 * Released under LGPL License. 6118 * 6119 * License: http://www.tinymce.com/license 6120 * Contributing: http://www.tinymce.com/contributing 6121 */ 6122 6123 /** 6124 * Schema validator class. 6125 * 6126 * @class tinymce.html.Schema 6127 * @example 6128 * if (tinymce.activeEditor.schema.isValidChild('p', 'span')) 6129 * alert('span is valid child of p.'); 6130 * 6131 * if (tinymce.activeEditor.schema.getElementRule('p')) 6132 * alert('P is a valid element.'); 6133 * 6134 * @class tinymce.html.Schema 6135 * @version 3.4 6136 */ 6137 define("tinymce/html/Schema", [ 6138 "tinymce/util/Tools" 6139 ], function(Tools) { 6140 var mapCache = {}; 6141 var makeMap = Tools.makeMap, each = Tools.each, extend = Tools.extend, explode = Tools.explode, inArray = Tools.inArray; 6142 6143 function split(items, delim) { 6144 return items ? items.split(delim || ' ') : []; 6145 } 6146 6147 /** 6148 * Builds a schema lookup table 6149 * 6150 * @private 6151 * @param {String} type html4, html5 or html5-strict schema type. 6152 * @return {Object} Schema lookup table. 6153 */ 6154 function compileSchema(type) { 6155 var schema = {}, globalAttributes, blockContent; 6156 var phrasingContent, flowContent, html4BlockContent, html4PhrasingContent; 6157 6158 function add(name, attributes, children) { 6159 var ni, i, attributesOrder, args = arguments; 6160 6161 function arrayToMap(array) { 6162 var map = {}, i, l; 6163 6164 for (i = 0, l = array.length; i < l; i++) { 6165 map[array[i]] = {}; 6166 } 6167 6168 return map; 6169 } 6170 6171 children = children || []; 6172 attributes = attributes || ""; 6173 6174 if (typeof(children) === "string") { 6175 children = split(children); 6176 } 6177 6178 // Split string children 6179 for (i = 3; i < args.length; i++) { 6180 if (typeof(args[i]) === "string") { 6181 args[i] = split(args[i]); 6182 } 6183 6184 children.push.apply(children, args[i]); 6185 } 6186 6187 name = split(name); 6188 ni = name.length; 6189 while (ni--) { 6190 attributesOrder = [].concat(globalAttributes, split(attributes)); 6191 schema[name[ni]] = { 6192 attributes: arrayToMap(attributesOrder), 6193 attributesOrder: attributesOrder, 6194 children: arrayToMap(children) 6195 }; 6196 } 6197 } 6198 6199 function addAttrs(name, attributes) { 6200 var ni, schemaItem, i, l; 6201 6202 name = split(name); 6203 ni = name.length; 6204 attributes = split(attributes); 6205 while (ni--) { 6206 schemaItem = schema[name[ni]]; 6207 for (i = 0, l = attributes.length; i < l; i++) { 6208 schemaItem.attributes[attributes[i]] = {}; 6209 schemaItem.attributesOrder.push(attributes[i]); 6210 } 6211 } 6212 } 6213 6214 // Use cached schema 6215 if (mapCache[type]) { 6216 return mapCache[type]; 6217 } 6218 6219 // Attributes present on all elements 6220 globalAttributes = split("id accesskey class dir lang style tabindex title"); 6221 6222 // Event attributes can be opt-in/opt-out 6223 /*eventAttributes = split("onabort onblur oncancel oncanplay oncanplaythrough onchange onclick onclose oncontextmenu oncuechange " + 6224 "ondblclick ondrag ondragend ondragenter ondragleave ondragover ondragstart ondrop ondurationchange onemptied onended " + 6225 "onerror onfocus oninput oninvalid onkeydown onkeypress onkeyup onload onloadeddata onloadedmetadata onloadstart " + 6226 "onmousedown onmousemove onmouseout onmouseover onmouseup onmousewheel onpause onplay onplaying onprogress onratechange " + 6227 "onreset onscroll onseeked onseeking onseeking onselect onshow onstalled onsubmit onsuspend ontimeupdate onvolumechange " + 6228 "onwaiting" 6229 );*/ 6230 6231 // Block content elements 6232 blockContent = split( 6233 "address blockquote div dl fieldset form h1 h2 h3 h4 h5 h6 hr menu ol p pre table ul" 6234 ); 6235 6236 // Phrasing content elements from the HTML5 spec (inline) 6237 phrasingContent = split( 6238 "a abbr b bdo br button cite code del dfn em embed i iframe img input ins kbd " + 6239 "label map noscript object q s samp script select small span strong sub sup " + 6240 "textarea u var #text #comment" 6241 ); 6242 6243 // Add HTML5 items to globalAttributes, blockContent, phrasingContent 6244 if (type != "html4") { 6245 globalAttributes.push.apply(globalAttributes, split("contenteditable contextmenu draggable dropzone " + 6246 "hidden spellcheck translate")); 6247 blockContent.push.apply(blockContent, split("article aside details dialog figure header footer hgroup section nav")); 6248 phrasingContent.push.apply(phrasingContent, split("audio canvas command datalist mark meter output progress time wbr " + 6249 "video ruby bdi keygen")); 6250 } 6251 6252 // Add HTML4 elements unless it's html5-strict 6253 if (type != "html5-strict") { 6254 globalAttributes.push("xml:lang"); 6255 6256 html4PhrasingContent = split("acronym applet basefont big font strike tt"); 6257 phrasingContent.push.apply(phrasingContent, html4PhrasingContent); 6258 6259 each(html4PhrasingContent, function(name) { 6260 add(name, "", phrasingContent); 6261 }); 6262 6263 html4BlockContent = split("center dir isindex noframes"); 6264 blockContent.push.apply(blockContent, html4BlockContent); 6265 6266 // Flow content elements from the HTML5 spec (block+inline) 6267 flowContent = [].concat(blockContent, phrasingContent); 6268 6269 each(html4BlockContent, function(name) { 6270 add(name, "", flowContent); 6271 }); 6272 } 6273 6274 // Flow content elements from the HTML5 spec (block+inline) 6275 flowContent = flowContent || [].concat(blockContent, phrasingContent); 6276 6277 // HTML4 base schema TODO: Move HTML5 specific attributes to HTML5 specific if statement 6278 // Schema items <element name>, <specific attributes>, <children ..> 6279 add("html", "manifest", "head body"); 6280 add("head", "", "base command link meta noscript script style title"); 6281 add("title hr noscript br"); 6282 add("base", "href target"); 6283 add("link", "href rel media hreflang type sizes hreflang"); 6284 add("meta", "name http-equiv content charset"); 6285 add("style", "media type scoped"); 6286 add("script", "src async defer type charset"); 6287 add("body", "onafterprint onbeforeprint onbeforeunload onblur onerror onfocus " + 6288 "onhashchange onload onmessage onoffline ononline onpagehide onpageshow " + 6289 "onpopstate onresize onscroll onstorage onunload", flowContent); 6290 add("address dt dd div caption", "", flowContent); 6291 add("h1 h2 h3 h4 h5 h6 pre p abbr code var samp kbd sub sup i b u bdo span legend em strong small s cite dfn", "", phrasingContent); 6292 add("blockquote", "cite", flowContent); 6293 add("ol", "reversed start type", "li"); 6294 add("ul", "", "li"); 6295 add("li", "value", flowContent); 6296 add("dl", "", "dt dd"); 6297 add("a", "href target rel media hreflang type", phrasingContent); 6298 add("q", "cite", phrasingContent); 6299 add("ins del", "cite datetime", flowContent); 6300 add("img", "src alt usemap ismap width height"); 6301 add("iframe", "src name width height", flowContent); 6302 add("embed", "src type width height"); 6303 add("object", "data type typemustmatch name usemap form width height", flowContent, "param"); 6304 add("param", "name value"); 6305 add("map", "name", flowContent, "area"); 6306 add("area", "alt coords shape href target rel media hreflang type"); 6307 add("table", "border", "caption colgroup thead tfoot tbody tr" + (type == "html4" ? " col" : "")); 6308 add("colgroup", "span", "col"); 6309 add("col", "span"); 6310 add("tbody thead tfoot", "", "tr"); 6311 add("tr", "", "td th"); 6312 add("td", "colspan rowspan headers", flowContent); 6313 add("th", "colspan rowspan headers scope abbr", flowContent); 6314 add("form", "accept-charset action autocomplete enctype method name novalidate target", flowContent); 6315 add("fieldset", "disabled form name", flowContent, "legend"); 6316 add("label", "form for", phrasingContent); 6317 add("input", "accept alt autocomplete checked dirname disabled form formaction formenctype formmethod formnovalidate " + 6318 "formtarget height list max maxlength min multiple name pattern readonly required size src step type value width" 6319 ); 6320 add("button", "disabled form formaction formenctype formmethod formnovalidate formtarget name type value", 6321 type == "html4" ? flowContent : phrasingContent); 6322 add("select", "disabled form multiple name required size", "option optgroup"); 6323 add("optgroup", "disabled label", "option"); 6324 add("option", "disabled label selected value"); 6325 add("textarea", "cols dirname disabled form maxlength name readonly required rows wrap"); 6326 add("menu", "type label", flowContent, "li"); 6327 add("noscript", "", flowContent); 6328 6329 // Extend with HTML5 elements 6330 if (type != "html4") { 6331 add("wbr"); 6332 add("ruby", "", phrasingContent, "rt rp"); 6333 add("figcaption", "", flowContent); 6334 add("mark rt rp summary bdi", "", phrasingContent); 6335 add("canvas", "width height", flowContent); 6336 add("video", "src crossorigin poster preload autoplay mediagroup loop " + 6337 "muted controls width height buffered", flowContent, "track source"); 6338 add("audio", "src crossorigin preload autoplay mediagroup loop muted controls buffered volume", flowContent, "track source"); 6339 add("source", "src type media"); 6340 add("track", "kind src srclang label default"); 6341 add("datalist", "", phrasingContent, "option"); 6342 add("article section nav aside header footer", "", flowContent); 6343 add("hgroup", "", "h1 h2 h3 h4 h5 h6"); 6344 add("figure", "", flowContent, "figcaption"); 6345 add("time", "datetime", phrasingContent); 6346 add("dialog", "open", flowContent); 6347 add("command", "type label icon disabled checked radiogroup command"); 6348 add("output", "for form name", phrasingContent); 6349 add("progress", "value max", phrasingContent); 6350 add("meter", "value min max low high optimum", phrasingContent); 6351 add("details", "open", flowContent, "summary"); 6352 add("keygen", "autofocus challenge disabled form keytype name"); 6353 } 6354 6355 // Extend with HTML4 attributes unless it's html5-strict 6356 if (type != "html5-strict") { 6357 addAttrs("script", "language xml:space"); 6358 addAttrs("style", "xml:space"); 6359 addAttrs("object", "declare classid codebase codetype archive standby align border hspace vspace"); 6360 addAttrs("param", "valuetype type"); 6361 addAttrs("a", "charset name rev shape coords"); 6362 addAttrs("br", "clear"); 6363 addAttrs("applet", "codebase archive code object alt name width height align hspace vspace"); 6364 addAttrs("img", "name longdesc align border hspace vspace"); 6365 addAttrs("iframe", "longdesc frameborder marginwidth marginheight scrolling align"); 6366 addAttrs("font basefont", "size color face"); 6367 addAttrs("input", "usemap align"); 6368 addAttrs("select", "onchange"); 6369 addAttrs("textarea"); 6370 addAttrs("h1 h2 h3 h4 h5 h6 div p legend caption", "align"); 6371 addAttrs("ul", "type compact"); 6372 addAttrs("li", "type"); 6373 addAttrs("ol dl menu dir", "compact"); 6374 addAttrs("pre", "width xml:space"); 6375 addAttrs("hr", "align noshade size width"); 6376 addAttrs("isindex", "prompt"); 6377 addAttrs("table", "summary width frame rules cellspacing cellpadding align bgcolor"); 6378 addAttrs("col", "width align char charoff valign"); 6379 addAttrs("colgroup", "width align char charoff valign"); 6380 addAttrs("thead", "align char charoff valign"); 6381 addAttrs("tr", "align char charoff valign bgcolor"); 6382 addAttrs("th", "axis align char charoff valign nowrap bgcolor width height"); 6383 addAttrs("form", "accept"); 6384 addAttrs("td", "abbr axis scope align char charoff valign nowrap bgcolor width height"); 6385 addAttrs("tfoot", "align char charoff valign"); 6386 addAttrs("tbody", "align char charoff valign"); 6387 addAttrs("area", "nohref"); 6388 addAttrs("body", "background bgcolor text link vlink alink"); 6389 } 6390 6391 // Extend with HTML5 attributes unless it's html4 6392 if (type != "html4") { 6393 addAttrs("input button select textarea", "autofocus"); 6394 addAttrs("input textarea", "placeholder"); 6395 addAttrs("a", "download"); 6396 addAttrs("link script img", "crossorigin"); 6397 addAttrs("iframe", "sandbox seamless allowfullscreen"); // Excluded: srcdoc 6398 } 6399 6400 // Special: iframe, ruby, video, audio, label 6401 6402 // Delete children of the same name from it's parent 6403 // For example: form can't have a child of the name form 6404 each(split('a form meter progress dfn'), function(name) { 6405 if (schema[name]) { 6406 delete schema[name].children[name]; 6407 } 6408 }); 6409 6410 // Delete header, footer, sectioning and heading content descendants 6411 /*each('dt th address', function(name) { 6412 delete schema[name].children[name]; 6413 });*/ 6414 6415 // Caption can't have tables 6416 delete schema.caption.children.table; 6417 6418 // TODO: LI:s can only have value if parent is OL 6419 6420 // TODO: Handle transparent elements 6421 // a ins del canvas map 6422 6423 mapCache[type] = schema; 6424 6425 return schema; 6426 } 6427 6428 /** 6429 * Constructs a new Schema instance. 6430 * 6431 * @constructor 6432 * @method Schema 6433 * @param {Object} settings Name/value settings object. 6434 */ 6435 return function(settings) { 6436 var self = this, elements = {}, children = {}, patternElements = [], validStyles, schemaItems; 6437 var whiteSpaceElementsMap, selfClosingElementsMap, shortEndedElementsMap, boolAttrMap; 6438 var blockElementsMap, nonEmptyElementsMap, textBlockElementsMap, customElementsMap = {}, specialElements = {}; 6439 6440 // Creates an lookup table map object for the specified option or the default value 6441 function createLookupTable(option, default_value, extendWith) { 6442 var value = settings[option]; 6443 6444 if (!value) { 6445 // Get cached default map or make it if needed 6446 value = mapCache[option]; 6447 6448 if (!value) { 6449 value = makeMap(default_value, ' ', makeMap(default_value.toUpperCase(), ' ')); 6450 value = extend(value, extendWith); 6451 6452 mapCache[option] = value; 6453 } 6454 } else { 6455 // Create custom map 6456 value = makeMap(value, /[, ]/, makeMap(value.toUpperCase(), /[, ]/)); 6457 } 6458 6459 return value; 6460 } 6461 6462 settings = settings || {}; 6463 schemaItems = compileSchema(settings.schema); 6464 6465 // Allow all elements and attributes if verify_html is set to false 6466 if (settings.verify_html === false) { 6467 settings.valid_elements = '*[*]'; 6468 } 6469 6470 // Build styles list 6471 if (settings.valid_styles) { 6472 validStyles = {}; 6473 6474 // Convert styles into a rule list 6475 each(settings.valid_styles, function(value, key) { 6476 validStyles[key] = explode(value); 6477 }); 6478 } 6479 6480 // Setup map objects 6481 whiteSpaceElementsMap = createLookupTable('whitespace_elements', 'pre script noscript style textarea video audio iframe object'); 6482 selfClosingElementsMap = createLookupTable('self_closing_elements', 'colgroup dd dt li option p td tfoot th thead tr'); 6483 shortEndedElementsMap = createLookupTable('short_ended_elements', 'area base basefont br col frame hr img input isindex link ' + 6484 'meta param embed source wbr track'); 6485 boolAttrMap = createLookupTable('boolean_attributes', 'checked compact declare defer disabled ismap multiple nohref noresize ' + 6486 'noshade nowrap readonly selected autoplay loop controls'); 6487 nonEmptyElementsMap = createLookupTable('non_empty_elements', 'td th iframe video audio object script', shortEndedElementsMap); 6488 textBlockElementsMap = createLookupTable('text_block_elements', 'h1 h2 h3 h4 h5 h6 p div address pre form ' + 6489 'blockquote center dir fieldset header footer article section hgroup aside nav figure'); 6490 blockElementsMap = createLookupTable('block_elements', 'hr table tbody thead tfoot ' + 6491 'th tr td li ol ul caption dl dt dd noscript menu isindex option ' + 6492 'datalist select optgroup', textBlockElementsMap); 6493 6494 each((settings.special || 'script noscript style textarea').split(' '), function(name) { 6495 specialElements[name] = new RegExp('<\/' + name + '[^>]*>','gi'); 6496 }); 6497 6498 // Converts a wildcard expression string to a regexp for example *a will become /.*a/. 6499 function patternToRegExp(str) { 6500 return new RegExp('^' + str.replace(/([?+*])/g, '.$1') + '$'); 6501 } 6502 6503 // Parses the specified valid_elements string and adds to the current rules 6504 // This function is a bit hard to read since it's heavily optimized for speed 6505 function addValidElements(valid_elements) { 6506 var ei, el, ai, al, matches, element, attr, attrData, elementName, attrName, attrType, attributes, attributesOrder, 6507 prefix, outputName, globalAttributes, globalAttributesOrder, key, value, 6508 elementRuleRegExp = /^([#+\-])?([^\[!\/]+)(?:\/([^\[!]+))?(?:(!?)\[([^\]]+)\])?$/, 6509 attrRuleRegExp = /^([!\-])?(\w+::\w+|[^=:<]+)?(?:([=:<])(.*))?$/, 6510 hasPatternsRegExp = /[*?+]/; 6511 6512 if (valid_elements) { 6513 // Split valid elements into an array with rules 6514 valid_elements = split(valid_elements, ','); 6515 6516 if (elements['@']) { 6517 globalAttributes = elements['@'].attributes; 6518 globalAttributesOrder = elements['@'].attributesOrder; 6519 } 6520 6521 // Loop all rules 6522 for (ei = 0, el = valid_elements.length; ei < el; ei++) { 6523 // Parse element rule 6524 matches = elementRuleRegExp.exec(valid_elements[ei]); 6525 if (matches) { 6526 // Setup local names for matches 6527 prefix = matches[1]; 6528 elementName = matches[2]; 6529 outputName = matches[3]; 6530 attrData = matches[5]; 6531 6532 // Create new attributes and attributesOrder 6533 attributes = {}; 6534 attributesOrder = []; 6535 6536 // Create the new element 6537 element = { 6538 attributes: attributes, 6539 attributesOrder: attributesOrder 6540 }; 6541 6542 // Padd empty elements prefix 6543 if (prefix === '#') { 6544 element.paddEmpty = true; 6545 } 6546 6547 // Remove empty elements prefix 6548 if (prefix === '-') { 6549 element.removeEmpty = true; 6550 } 6551 6552 if (matches[4] === '!') { 6553 element.removeEmptyAttrs = true; 6554 } 6555 6556 // Copy attributes from global rule into current rule 6557 if (globalAttributes) { 6558 for (key in globalAttributes) { 6559 attributes[key] = globalAttributes[key]; 6560 } 6561 6562 attributesOrder.push.apply(attributesOrder, globalAttributesOrder); 6563 } 6564 6565 // Attributes defined 6566 if (attrData) { 6567 attrData = split(attrData, '|'); 6568 for (ai = 0, al = attrData.length; ai < al; ai++) { 6569 matches = attrRuleRegExp.exec(attrData[ai]); 6570 if (matches) { 6571 attr = {}; 6572 attrType = matches[1]; 6573 attrName = matches[2].replace(/::/g, ':'); 6574 prefix = matches[3]; 6575 value = matches[4]; 6576 6577 // Required 6578 if (attrType === '!') { 6579 element.attributesRequired = element.attributesRequired || []; 6580 element.attributesRequired.push(attrName); 6581 attr.required = true; 6582 } 6583 6584 // Denied from global 6585 if (attrType === '-') { 6586 delete attributes[attrName]; 6587 attributesOrder.splice(inArray(attributesOrder, attrName), 1); 6588 continue; 6589 } 6590 6591 // Default value 6592 if (prefix) { 6593 // Default value 6594 if (prefix === '=') { 6595 element.attributesDefault = element.attributesDefault || []; 6596 element.attributesDefault.push({name: attrName, value: value}); 6597 attr.defaultValue = value; 6598 } 6599 6600 // Forced value 6601 if (prefix === ':') { 6602 element.attributesForced = element.attributesForced || []; 6603 element.attributesForced.push({name: attrName, value: value}); 6604 attr.forcedValue = value; 6605 } 6606 6607 // Required values 6608 if (prefix === '<') { 6609 attr.validValues = makeMap(value, '?'); 6610 } 6611 } 6612 6613 // Check for attribute patterns 6614 if (hasPatternsRegExp.test(attrName)) { 6615 element.attributePatterns = element.attributePatterns || []; 6616 attr.pattern = patternToRegExp(attrName); 6617 element.attributePatterns.push(attr); 6618 } else { 6619 // Add attribute to order list if it doesn't already exist 6620 if (!attributes[attrName]) { 6621 attributesOrder.push(attrName); 6622 } 6623 6624 attributes[attrName] = attr; 6625 } 6626 } 6627 } 6628 } 6629 6630 // Global rule, store away these for later usage 6631 if (!globalAttributes && elementName == '@') { 6632 globalAttributes = attributes; 6633 globalAttributesOrder = attributesOrder; 6634 } 6635 6636 // Handle substitute elements such as b/strong 6637 if (outputName) { 6638 element.outputName = elementName; 6639 elements[outputName] = element; 6640 } 6641 6642 // Add pattern or exact element 6643 if (hasPatternsRegExp.test(elementName)) { 6644 element.pattern = patternToRegExp(elementName); 6645 patternElements.push(element); 6646 } else { 6647 elements[elementName] = element; 6648 } 6649 } 6650 } 6651 } 6652 } 6653 6654 function setValidElements(valid_elements) { 6655 elements = {}; 6656 patternElements = []; 6657 6658 addValidElements(valid_elements); 6659 6660 each(schemaItems, function(element, name) { 6661 children[name] = element.children; 6662 }); 6663 } 6664 6665 // Adds custom non HTML elements to the schema 6666 function addCustomElements(custom_elements) { 6667 var customElementRegExp = /^(~)?(.+)$/; 6668 6669 if (custom_elements) { 6670 // Flush cached items since we are altering the default maps 6671 mapCache.text_block_elements = mapCache.block_elements = null; 6672 6673 each(split(custom_elements, ','), function(rule) { 6674 var matches = customElementRegExp.exec(rule), 6675 inline = matches[1] === '~', 6676 cloneName = inline ? 'span' : 'div', 6677 name = matches[2]; 6678 6679 children[name] = children[cloneName]; 6680 customElementsMap[name] = cloneName; 6681 6682 // If it's not marked as inline then add it to valid block elements 6683 if (!inline) { 6684 blockElementsMap[name.toUpperCase()] = {}; 6685 blockElementsMap[name] = {}; 6686 } 6687 6688 // Add elements clone if needed 6689 if (!elements[name]) { 6690 var customRule = elements[cloneName]; 6691 6692 customRule = extend({}, customRule); 6693 delete customRule.removeEmptyAttrs; 6694 delete customRule.removeEmpty; 6695 6696 elements[name] = customRule; 6697 } 6698 6699 // Add custom elements at span/div positions 6700 each(children, function(element, elmName) { 6701 if (element[cloneName]) { 6702 children[elmName] = element = extend({}, children[elmName]); 6703 element[name] = element[cloneName]; 6704 } 6705 }); 6706 }); 6707 } 6708 } 6709 6710 // Adds valid children to the schema object 6711 function addValidChildren(valid_children) { 6712 var childRuleRegExp = /^([+\-]?)(\w+)\[([^\]]+)\]$/; 6713 6714 if (valid_children) { 6715 each(split(valid_children, ','), function(rule) { 6716 var matches = childRuleRegExp.exec(rule), parent, prefix; 6717 6718 if (matches) { 6719 prefix = matches[1]; 6720 6721 // Add/remove items from default 6722 if (prefix) { 6723 parent = children[matches[2]]; 6724 } else { 6725 parent = children[matches[2]] = {'#comment': {}}; 6726 } 6727 6728 parent = children[matches[2]]; 6729 6730 each(split(matches[3], '|'), function(child) { 6731 if (prefix === '-') { 6732 // Clone the element before we delete 6733 // things in it to not mess up default schemas 6734 children[matches[2]] = parent = extend({}, children[matches[2]]); 6735 6736 delete parent[child]; 6737 } else { 6738 parent[child] = {}; 6739 } 6740 }); 6741 } 6742 }); 6743 } 6744 } 6745 6746 function getElementRule(name) { 6747 var element = elements[name], i; 6748 6749 // Exact match found 6750 if (element) { 6751 return element; 6752 } 6753 6754 // No exact match then try the patterns 6755 i = patternElements.length; 6756 while (i--) { 6757 element = patternElements[i]; 6758 6759 if (element.pattern.test(name)) { 6760 return element; 6761 } 6762 } 6763 } 6764 6765 if (!settings.valid_elements) { 6766 // No valid elements defined then clone the elements from the schema spec 6767 each(schemaItems, function(element, name) { 6768 elements[name] = { 6769 attributes: element.attributes, 6770 attributesOrder: element.attributesOrder 6771 }; 6772 6773 children[name] = element.children; 6774 }); 6775 6776 // Switch these on HTML4 6777 if (settings.schema != "html5") { 6778 each(split('strong/b em/i'), function(item) { 6779 item = split(item, '/'); 6780 elements[item[1]].outputName = item[0]; 6781 }); 6782 } 6783 6784 // Add default alt attribute for images 6785 elements.img.attributesDefault = [{name: 'alt', value: ''}]; 6786 6787 // Remove these if they are empty by default 6788 each(split('ol ul sub sup blockquote span font a table tbody tr strong em b i'), function(name) { 6789 if (elements[name]) { 6790 elements[name].removeEmpty = true; 6791 } 6792 }); 6793 6794 // Padd these by default 6795 each(split('p h1 h2 h3 h4 h5 h6 th td pre div address caption'), function(name) { 6796 elements[name].paddEmpty = true; 6797 }); 6798 6799 // Remove these if they have no attributes 6800 each(split('span'), function(name) { 6801 elements[name].removeEmptyAttrs = true; 6802 }); 6803 6804 // Remove these by default 6805 // TODO: Reenable in 4.1 6806 /*each(split('script style'), function(name) { 6807 delete elements[name]; 6808 });*/ 6809 } else { 6810 setValidElements(settings.valid_elements); 6811 } 6812 6813 addCustomElements(settings.custom_elements); 6814 addValidChildren(settings.valid_children); 6815 addValidElements(settings.extended_valid_elements); 6816 6817 // Todo: Remove this when we fix list handling to be valid 6818 addValidChildren('+ol[ul|ol],+ul[ul|ol]'); 6819 6820 // Delete invalid elements 6821 if (settings.invalid_elements) { 6822 each(explode(settings.invalid_elements), function(item) { 6823 if (elements[item]) { 6824 delete elements[item]; 6825 } 6826 }); 6827 } 6828 6829 // If the user didn't allow span only allow internal spans 6830 if (!getElementRule('span')) { 6831 addValidElements('span[!data-mce-type|*]'); 6832 } 6833 6834 /** 6835 * Name/value map object with valid parents and children to those parents. 6836 * 6837 * @example 6838 * children = { 6839 * div:{p:{}, h1:{}} 6840 * }; 6841 * @field children 6842 * @type Object 6843 */ 6844 self.children = children; 6845 6846 /** 6847 * Name/value map object with valid styles for each element. 6848 * 6849 * @field styles 6850 * @type Object 6851 */ 6852 self.styles = validStyles; 6853 6854 /** 6855 * Returns a map with boolean attributes. 6856 * 6857 * @method getBoolAttrs 6858 * @return {Object} Name/value lookup map for boolean attributes. 6859 */ 6860 self.getBoolAttrs = function() { 6861 return boolAttrMap; 6862 }; 6863 6864 /** 6865 * Returns a map with block elements. 6866 * 6867 * @method getBlockElements 6868 * @return {Object} Name/value lookup map for block elements. 6869 */ 6870 self.getBlockElements = function() { 6871 return blockElementsMap; 6872 }; 6873 6874 /** 6875 * Returns a map with text block elements. Such as: p,h1-h6,div,address 6876 * 6877 * @method getTextBlockElements 6878 * @return {Object} Name/value lookup map for block elements. 6879 */ 6880 self.getTextBlockElements = function() { 6881 return textBlockElementsMap; 6882 }; 6883 6884 /** 6885 * Returns a map with short ended elements such as BR or IMG. 6886 * 6887 * @method getShortEndedElements 6888 * @return {Object} Name/value lookup map for short ended elements. 6889 */ 6890 self.getShortEndedElements = function() { 6891 return shortEndedElementsMap; 6892 }; 6893 6894 /** 6895 * Returns a map with self closing tags such as <li>. 6896 * 6897 * @method getSelfClosingElements 6898 * @return {Object} Name/value lookup map for self closing tags elements. 6899 */ 6900 self.getSelfClosingElements = function() { 6901 return selfClosingElementsMap; 6902 }; 6903 6904 /** 6905 * Returns a map with elements that should be treated as contents regardless if it has text 6906 * content in them or not such as TD, VIDEO or IMG. 6907 * 6908 * @method getNonEmptyElements 6909 * @return {Object} Name/value lookup map for non empty elements. 6910 */ 6911 self.getNonEmptyElements = function() { 6912 return nonEmptyElementsMap; 6913 }; 6914 6915 /** 6916 * Returns a map with elements where white space is to be preserved like PRE or SCRIPT. 6917 * 6918 * @method getWhiteSpaceElements 6919 * @return {Object} Name/value lookup map for white space elements. 6920 */ 6921 self.getWhiteSpaceElements = function() { 6922 return whiteSpaceElementsMap; 6923 }; 6924 6925 /** 6926 * Returns a map with special elements. These are elements that needs to be parsed 6927 * in a special way such as script, style, textarea etc. The map object values 6928 * are regexps used to find the end of the element. 6929 * 6930 * @method getSpecialElements 6931 * @return {Object} Name/value lookup map for special elements. 6932 */ 6933 self.getSpecialElements = function() { 6934 return specialElements; 6935 }; 6936 6937 /** 6938 * Returns true/false if the specified element and it's child is valid or not 6939 * according to the schema. 6940 * 6941 * @method isValidChild 6942 * @param {String} name Element name to check for. 6943 * @param {String} child Element child to verify. 6944 * @return {Boolean} True/false if the element is a valid child of the specified parent. 6945 */ 6946 self.isValidChild = function(name, child) { 6947 var parent = children[name]; 6948 6949 return !!(parent && parent[child]); 6950 }; 6951 6952 /** 6953 * Returns true/false if the specified element name and optional attribute is 6954 * valid according to the schema. 6955 * 6956 * @method isValid 6957 * @param {String} name Name of element to check. 6958 * @param {String} attr Optional attribute name to check for. 6959 * @return {Boolean} True/false if the element and attribute is valid. 6960 */ 6961 self.isValid = function(name, attr) { 6962 var attrPatterns, i, rule = getElementRule(name); 6963 6964 // Check if it's a valid element 6965 if (rule) { 6966 if (attr) { 6967 // Check if attribute name exists 6968 if (rule.attributes[attr]) { 6969 return true; 6970 } 6971 6972 // Check if attribute matches a regexp pattern 6973 attrPatterns = rule.attributePatterns; 6974 if (attrPatterns) { 6975 i = attrPatterns.length; 6976 while (i--) { 6977 if (attrPatterns[i].pattern.test(name)) { 6978 return true; 6979 } 6980 } 6981 } 6982 } else { 6983 return true; 6984 } 6985 } 6986 6987 // No match 6988 return false; 6989 }; 6990 6991 /** 6992 * Returns true/false if the specified element is valid or not 6993 * according to the schema. 6994 * 6995 * @method getElementRule 6996 * @param {String} name Element name to check for. 6997 * @return {Object} Element object or undefined if the element isn't valid. 6998 */ 6999 self.getElementRule = getElementRule; 7000 7001 /** 7002 * Returns an map object of all custom elements. 7003 * 7004 * @method getCustomElements 7005 * @return {Object} Name/value map object of all custom elements. 7006 */ 7007 self.getCustomElements = function() { 7008 return customElementsMap; 7009 }; 7010 7011 /** 7012 * Parses a valid elements string and adds it to the schema. The valid elements 7013 * format is for example "element[attr=default|otherattr]". 7014 * Existing rules will be replaced with the ones specified, so this extends the schema. 7015 * 7016 * @method addValidElements 7017 * @param {String} valid_elements String in the valid elements format to be parsed. 7018 */ 7019 self.addValidElements = addValidElements; 7020 7021 /** 7022 * Parses a valid elements string and sets it to the schema. The valid elements 7023 * format is for example "element[attr=default|otherattr]". 7024 * Existing rules will be replaced with the ones specified, so this extends the schema. 7025 * 7026 * @method setValidElements 7027 * @param {String} valid_elements String in the valid elements format to be parsed. 7028 */ 7029 self.setValidElements = setValidElements; 7030 7031 /** 7032 * Adds custom non HTML elements to the schema. 7033 * 7034 * @method addCustomElements 7035 * @param {String} custom_elements Comma separated list of custom elements to add. 7036 */ 7037 self.addCustomElements = addCustomElements; 7038 7039 /** 7040 * Parses a valid children string and adds them to the schema structure. The valid children 7041 * format is for example: "element[child1|child2]". 7042 * 7043 * @method addValidChildren 7044 * @param {String} valid_children Valid children elements string to parse 7045 */ 7046 self.addValidChildren = addValidChildren; 7047 7048 self.elements = elements; 7049 }; 7050 }); 7051 7052 // Included from: js/tinymce/classes/html/SaxParser.js 7053 7054 /** 7055 * SaxParser.js 7056 * 7057 * Copyright, Moxiecode Systems AB 7058 * Released under LGPL License. 7059 * 7060 * License: http://www.tinymce.com/license 7061 * Contributing: http://www.tinymce.com/contributing 7062 */ 7063 7064 /*eslint max-depth:[2, 9] */ 7065 7066 /** 7067 * This class parses HTML code using pure JavaScript and executes various events for each item it finds. It will 7068 * always execute the events in the right order for tag soup code like <b><p></b></p>. It will also remove elements 7069 * and attributes that doesn't fit the schema if the validate setting is enabled. 7070 * 7071 * @example 7072 * var parser = new tinymce.html.SaxParser({ 7073 * validate: true, 7074 * 7075 * comment: function(text) { 7076 * console.log('Comment:', text); 7077 * }, 7078 * 7079 * cdata: function(text) { 7080 * console.log('CDATA:', text); 7081 * }, 7082 * 7083 * text: function(text, raw) { 7084 * console.log('Text:', text, 'Raw:', raw); 7085 * }, 7086 * 7087 * start: function(name, attrs, empty) { 7088 * console.log('Start:', name, attrs, empty); 7089 * }, 7090 * 7091 * end: function(name) { 7092 * console.log('End:', name); 7093 * }, 7094 * 7095 * pi: function(name, text) { 7096 * console.log('PI:', name, text); 7097 * }, 7098 * 7099 * doctype: function(text) { 7100 * console.log('DocType:', text); 7101 * } 7102 * }, schema); 7103 * @class tinymce.html.SaxParser 7104 * @version 3.4 7105 */ 7106 define("tinymce/html/SaxParser", [ 7107 "tinymce/html/Schema", 7108 "tinymce/html/Entities", 7109 "tinymce/util/Tools" 7110 ], function(Schema, Entities, Tools) { 7111 var each = Tools.each; 7112 7113 /** 7114 * Constructs a new SaxParser instance. 7115 * 7116 * @constructor 7117 * @method SaxParser 7118 * @param {Object} settings Name/value collection of settings. comment, cdata, text, start and end are callbacks. 7119 * @param {tinymce.html.Schema} schema HTML Schema class to use when parsing. 7120 */ 7121 return function(settings, schema) { 7122 var self = this; 7123 7124 function noop() {} 7125 7126 settings = settings || {}; 7127 self.schema = schema = schema || new Schema(); 7128 7129 if (settings.fix_self_closing !== false) { 7130 settings.fix_self_closing = true; 7131 } 7132 7133 // Add handler functions from settings and setup default handlers 7134 each('comment cdata text start end pi doctype'.split(' '), function(name) { 7135 if (name) { 7136 self[name] = settings[name] || noop; 7137 } 7138 }); 7139 7140 /** 7141 * Parses the specified HTML string and executes the callbacks for each item it finds. 7142 * 7143 * @example 7144 * new SaxParser({...}).parse('<b>text</b>'); 7145 * @method parse 7146 * @param {String} html Html string to sax parse. 7147 */ 7148 self.parse = function(html) { 7149 var self = this, matches, index = 0, value, endRegExp, stack = [], attrList, i, text, name; 7150 var isInternalElement, removeInternalElements, shortEndedElements, fillAttrsMap, isShortEnded; 7151 var validate, elementRule, isValidElement, attr, attribsValue, validAttributesMap, validAttributePatterns; 7152 var attributesRequired, attributesDefault, attributesForced; 7153 var anyAttributesRequired, selfClosing, tokenRegExp, attrRegExp, specialElements, attrValue, idCount = 0; 7154 var decode = Entities.decode, fixSelfClosing, filteredUrlAttrs = Tools.makeMap('src,href,data,background,formaction,poster'); 7155 var scriptUriRegExp = /((java|vb)script|mhtml):/i, dataUriRegExp = /^data:/i; 7156 7157 function processEndTag(name) { 7158 var pos, i; 7159 7160 // Find position of parent of the same type 7161 pos = stack.length; 7162 while (pos--) { 7163 if (stack[pos].name === name) { 7164 break; 7165 } 7166 } 7167 7168 // Found parent 7169 if (pos >= 0) { 7170 // Close all the open elements 7171 for (i = stack.length - 1; i >= pos; i--) { 7172 name = stack[i]; 7173 7174 if (name.valid) { 7175 self.end(name.name); 7176 } 7177 } 7178 7179 // Remove the open elements from the stack 7180 stack.length = pos; 7181 } 7182 } 7183 7184 function parseAttribute(match, name, value, val2, val3) { 7185 var attrRule, i, trimRegExp = /[\s\u0000-\u001F]+/g; 7186 7187 name = name.toLowerCase(); 7188 value = name in fillAttrsMap ? name : decode(value || val2 || val3 || ''); // Handle boolean attribute than value attribute 7189 7190 // Validate name and value pass through all data- attributes 7191 if (validate && !isInternalElement && name.indexOf('data-') !== 0) { 7192 attrRule = validAttributesMap[name]; 7193 7194 // Find rule by pattern matching 7195 if (!attrRule && validAttributePatterns) { 7196 i = validAttributePatterns.length; 7197 while (i--) { 7198 attrRule = validAttributePatterns[i]; 7199 if (attrRule.pattern.test(name)) { 7200 break; 7201 } 7202 } 7203 7204 // No rule matched 7205 if (i === -1) { 7206 attrRule = null; 7207 } 7208 } 7209 7210 // No attribute rule found 7211 if (!attrRule) { 7212 return; 7213 } 7214 7215 // Validate value 7216 if (attrRule.validValues && !(value in attrRule.validValues)) { 7217 return; 7218 } 7219 } 7220 7221 // Block any javascript: urls or non image data uris 7222 if (filteredUrlAttrs[name] && !settings.allow_script_urls) { 7223 var uri = value.replace(trimRegExp, ''); 7224 7225 try { 7226 // Might throw malformed URI sequence 7227 uri = decodeURIComponent(uri); 7228 } catch (ex) { 7229 // Fallback to non UTF-8 decoder 7230 uri = unescape(uri); 7231 } 7232 7233 if (scriptUriRegExp.test(uri)) { 7234 return; 7235 } 7236 7237 if (!settings.allow_html_data_urls && dataUriRegExp.test(uri) && !/^data:image\//i.test(uri)) { 7238 return; 7239 } 7240 } 7241 7242 // Add attribute to list and map 7243 attrList.map[name] = value; 7244 attrList.push({ 7245 name: name, 7246 value: value 7247 }); 7248 } 7249 7250 // Precompile RegExps and map objects 7251 tokenRegExp = new RegExp('<(?:' + 7252 '(?:!--([\\w\\W]*?)-->)|' + // Comment 7253 '(?:!\\[CDATA\\[([\\w\\W]*?)\\]\\]>)|' + // CDATA 7254 '(?:!DOCTYPE([\\w\\W]*?)>)|' + // DOCTYPE 7255 '(?:\\?([^\\s\\/<>]+) ?([\\w\\W]*?)[?/]>)|' + // PI 7256 '(?:\\/([^>]+)>)|' + // End element 7257 '(?:([A-Za-z0-9\\-\\:\\.]+)((?:\\s+[^"\'>]+(?:(?:"[^"]*")|(?:\'[^\']*\')|[^>]*))*|\\/|\\s+)>)' + // Start element 7258 ')', 'g'); 7259 7260 attrRegExp = /([\w:\-]+)(?:\s*=\s*(?:(?:\"((?:[^\"])*)\")|(?:\'((?:[^\'])*)\')|([^>\s]+)))?/g; 7261 7262 // Setup lookup tables for empty elements and boolean attributes 7263 shortEndedElements = schema.getShortEndedElements(); 7264 selfClosing = settings.self_closing_elements || schema.getSelfClosingElements(); 7265 fillAttrsMap = schema.getBoolAttrs(); 7266 validate = settings.validate; 7267 removeInternalElements = settings.remove_internals; 7268 fixSelfClosing = settings.fix_self_closing; 7269 specialElements = schema.getSpecialElements(); 7270 7271 while ((matches = tokenRegExp.exec(html))) { 7272 // Text 7273 if (index < matches.index) { 7274 self.text(decode(html.substr(index, matches.index - index))); 7275 } 7276 7277 if ((value = matches[6])) { // End element 7278 value = value.toLowerCase(); 7279 7280 // IE will add a ":" in front of elements it doesn't understand like custom elements or HTML5 elements 7281 if (value.charAt(0) === ':') { 7282 value = value.substr(1); 7283 } 7284 7285 processEndTag(value); 7286 } else if ((value = matches[7])) { // Start element 7287 value = value.toLowerCase(); 7288 7289 // IE will add a ":" in front of elements it doesn't understand like custom elements or HTML5 elements 7290 if (value.charAt(0) === ':') { 7291 value = value.substr(1); 7292 } 7293 7294 isShortEnded = value in shortEndedElements; 7295 7296 // Is self closing tag for example an <li> after an open <li> 7297 if (fixSelfClosing && selfClosing[value] && stack.length > 0 && stack[stack.length - 1].name === value) { 7298 processEndTag(value); 7299 } 7300 7301 // Validate element 7302 if (!validate || (elementRule = schema.getElementRule(value))) { 7303 isValidElement = true; 7304 7305 // Grab attributes map and patters when validation is enabled 7306 if (validate) { 7307 validAttributesMap = elementRule.attributes; 7308 validAttributePatterns = elementRule.attributePatterns; 7309 } 7310 7311 // Parse attributes 7312 if ((attribsValue = matches[8])) { 7313 isInternalElement = attribsValue.indexOf('data-mce-type') !== -1; // Check if the element is an internal element 7314 7315 // If the element has internal attributes then remove it if we are told to do so 7316 if (isInternalElement && removeInternalElements) { 7317 isValidElement = false; 7318 } 7319 7320 attrList = []; 7321 attrList.map = {}; 7322 7323 attribsValue.replace(attrRegExp, parseAttribute); 7324 } else { 7325 attrList = []; 7326 attrList.map = {}; 7327 } 7328 7329 // Process attributes if validation is enabled 7330 if (validate && !isInternalElement) { 7331 attributesRequired = elementRule.attributesRequired; 7332 attributesDefault = elementRule.attributesDefault; 7333 attributesForced = elementRule.attributesForced; 7334 anyAttributesRequired = elementRule.removeEmptyAttrs; 7335 7336 // Check if any attribute exists 7337 if (anyAttributesRequired && !attrList.length) { 7338 isValidElement = false; 7339 } 7340 7341 // Handle forced attributes 7342 if (attributesForced) { 7343 i = attributesForced.length; 7344 while (i--) { 7345 attr = attributesForced[i]; 7346 name = attr.name; 7347 attrValue = attr.value; 7348 7349 if (attrValue === '{$uid}') { 7350 attrValue = 'mce_' + idCount++; 7351 } 7352 7353 attrList.map[name] = attrValue; 7354 attrList.push({name: name, value: attrValue}); 7355 } 7356 } 7357 7358 // Handle default attributes 7359 if (attributesDefault) { 7360 i = attributesDefault.length; 7361 while (i--) { 7362 attr = attributesDefault[i]; 7363 name = attr.name; 7364 7365 if (!(name in attrList.map)) { 7366 attrValue = attr.value; 7367 7368 if (attrValue === '{$uid}') { 7369 attrValue = 'mce_' + idCount++; 7370 } 7371 7372 attrList.map[name] = attrValue; 7373 attrList.push({name: name, value: attrValue}); 7374 } 7375 } 7376 } 7377 7378 // Handle required attributes 7379 if (attributesRequired) { 7380 i = attributesRequired.length; 7381 while (i--) { 7382 if (attributesRequired[i] in attrList.map) { 7383 break; 7384 } 7385 } 7386 7387 // None of the required attributes where found 7388 if (i === -1) { 7389 isValidElement = false; 7390 } 7391 } 7392 7393 // Invalidate element if it's marked as bogus 7394 if (attrList.map['data-mce-bogus']) { 7395 isValidElement = false; 7396 } 7397 } 7398 7399 if (isValidElement) { 7400 self.start(value, attrList, isShortEnded); 7401 } 7402 } else { 7403 isValidElement = false; 7404 } 7405 7406 // Treat script, noscript and style a bit different since they may include code that looks like elements 7407 if ((endRegExp = specialElements[value])) { 7408 endRegExp.lastIndex = index = matches.index + matches[0].length; 7409 7410 if ((matches = endRegExp.exec(html))) { 7411 if (isValidElement) { 7412 text = html.substr(index, matches.index - index); 7413 } 7414 7415 index = matches.index + matches[0].length; 7416 } else { 7417 text = html.substr(index); 7418 index = html.length; 7419 } 7420 7421 if (isValidElement) { 7422 if (text.length > 0) { 7423 self.text(text, true); 7424 } 7425 7426 self.end(value); 7427 } 7428 7429 tokenRegExp.lastIndex = index; 7430 continue; 7431 } 7432 7433 // Push value on to stack 7434 if (!isShortEnded) { 7435 if (!attribsValue || attribsValue.indexOf('/') != attribsValue.length - 1) { 7436 stack.push({name: value, valid: isValidElement}); 7437 } else if (isValidElement) { 7438 self.end(value); 7439 } 7440 } 7441 } else if ((value = matches[1])) { // Comment 7442 // Padd comment value to avoid browsers from parsing invalid comments as HTML 7443 if (value.charAt(0) === '>') { 7444 value = ' ' + value; 7445 } 7446 7447 if (!settings.allow_conditional_comments && value.substr(0, 3) === '[if') { 7448 value = ' ' + value; 7449 } 7450 7451 self.comment(value); 7452 } else if ((value = matches[2])) { // CDATA 7453 self.cdata(value); 7454 } else if ((value = matches[3])) { // DOCTYPE 7455 self.doctype(value); 7456 } else if ((value = matches[4])) { // PI 7457 self.pi(value, matches[5]); 7458 } 7459 7460 index = matches.index + matches[0].length; 7461 } 7462 7463 // Text 7464 if (index < html.length) { 7465 self.text(decode(html.substr(index))); 7466 } 7467 7468 // Close any open elements 7469 for (i = stack.length - 1; i >= 0; i--) { 7470 value = stack[i]; 7471 7472 if (value.valid) { 7473 self.end(value.name); 7474 } 7475 } 7476 }; 7477 }; 7478 }); 7479 7480 // Included from: js/tinymce/classes/html/DomParser.js 7481 7482 /** 7483 * DomParser.js 7484 * 7485 * Copyright, Moxiecode Systems AB 7486 * Released under LGPL License. 7487 * 7488 * License: http://www.tinymce.com/license 7489 * Contributing: http://www.tinymce.com/contributing 7490 */ 7491 7492 /** 7493 * This class parses HTML code into a DOM like structure of nodes it will remove redundant whitespace and make 7494 * sure that the node tree is valid according to the specified schema. 7495 * So for example: <p>a<p>b</p>c</p> will become <p>a</p><p>b</p><p>c</p> 7496 * 7497 * @example 7498 * var parser = new tinymce.html.DomParser({validate: true}, schema); 7499 * var rootNode = parser.parse('<h1>content</h1>'); 7500 * 7501 * @class tinymce.html.DomParser 7502 * @version 3.4 7503 */ 7504 define("tinymce/html/DomParser", [ 7505 "tinymce/html/Node", 7506 "tinymce/html/Schema", 7507 "tinymce/html/SaxParser", 7508 "tinymce/util/Tools" 7509 ], function(Node, Schema, SaxParser, Tools) { 7510 var makeMap = Tools.makeMap, each = Tools.each, explode = Tools.explode, extend = Tools.extend; 7511 7512 /** 7513 * Constructs a new DomParser instance. 7514 * 7515 * @constructor 7516 * @method DomParser 7517 * @param {Object} settings Name/value collection of settings. comment, cdata, text, start and end are callbacks. 7518 * @param {tinymce.html.Schema} schema HTML Schema class to use when parsing. 7519 */ 7520 return function(settings, schema) { 7521 var self = this, nodeFilters = {}, attributeFilters = [], matchedNodes = {}, matchedAttributes = {}; 7522 7523 settings = settings || {}; 7524 settings.validate = "validate" in settings ? settings.validate : true; 7525 settings.root_name = settings.root_name || 'body'; 7526 self.schema = schema = schema || new Schema(); 7527 7528 function fixInvalidChildren(nodes) { 7529 var ni, node, parent, parents, newParent, currentNode, tempNode, childNode, i; 7530 var nonEmptyElements, nonSplitableElements, textBlockElements, sibling, nextNode; 7531 7532 nonSplitableElements = makeMap('tr,td,th,tbody,thead,tfoot,table'); 7533 nonEmptyElements = schema.getNonEmptyElements(); 7534 textBlockElements = schema.getTextBlockElements(); 7535 7536 for (ni = 0; ni < nodes.length; ni++) { 7537 node = nodes[ni]; 7538 7539 // Already removed or fixed 7540 if (!node.parent || node.fixed) { 7541 continue; 7542 } 7543 7544 // If the invalid element is a text block and the text block is within a parent LI element 7545 // Then unwrap the first text block and convert other sibling text blocks to LI elements similar to Word/Open Office 7546 if (textBlockElements[node.name] && node.parent.name == 'li') { 7547 // Move sibling text blocks after LI element 7548 sibling = node.next; 7549 while (sibling) { 7550 if (textBlockElements[sibling.name]) { 7551 sibling.name = 'li'; 7552 sibling.fixed = true; 7553 node.parent.insert(sibling, node.parent); 7554 } else { 7555 break; 7556 } 7557 7558 sibling = sibling.next; 7559 } 7560 7561 // Unwrap current text block 7562 node.unwrap(node); 7563 continue; 7564 } 7565 7566 // Get list of all parent nodes until we find a valid parent to stick the child into 7567 parents = [node]; 7568 for (parent = node.parent; parent && !schema.isValidChild(parent.name, node.name) && 7569 !nonSplitableElements[parent.name]; parent = parent.parent) { 7570 parents.push(parent); 7571 } 7572 7573 // Found a suitable parent 7574 if (parent && parents.length > 1) { 7575 // Reverse the array since it makes looping easier 7576 parents.reverse(); 7577 7578 // Clone the related parent and insert that after the moved node 7579 newParent = currentNode = self.filterNode(parents[0].clone()); 7580 7581 // Start cloning and moving children on the left side of the target node 7582 for (i = 0; i < parents.length - 1; i++) { 7583 if (schema.isValidChild(currentNode.name, parents[i].name)) { 7584 tempNode = self.filterNode(parents[i].clone()); 7585 currentNode.append(tempNode); 7586 } else { 7587 tempNode = currentNode; 7588 } 7589 7590 for (childNode = parents[i].firstChild; childNode && childNode != parents[i + 1]; ) { 7591 nextNode = childNode.next; 7592 tempNode.append(childNode); 7593 childNode = nextNode; 7594 } 7595 7596 currentNode = tempNode; 7597 } 7598 7599 if (!newParent.isEmpty(nonEmptyElements)) { 7600 parent.insert(newParent, parents[0], true); 7601 parent.insert(node, newParent); 7602 } else { 7603 parent.insert(node, parents[0], true); 7604 } 7605 7606 // Check if the element is empty by looking through it's contents and special treatment for <p><br /></p> 7607 parent = parents[0]; 7608 if (parent.isEmpty(nonEmptyElements) || parent.firstChild === parent.lastChild && parent.firstChild.name === 'br') { 7609 parent.empty().remove(); 7610 } 7611 } else if (node.parent) { 7612 // If it's an LI try to find a UL/OL for it or wrap it 7613 if (node.name === 'li') { 7614 sibling = node.prev; 7615 if (sibling && (sibling.name === 'ul' || sibling.name === 'ul')) { 7616 sibling.append(node); 7617 continue; 7618 } 7619 7620 sibling = node.next; 7621 if (sibling && (sibling.name === 'ul' || sibling.name === 'ul')) { 7622 sibling.insert(node, sibling.firstChild, true); 7623 continue; 7624 } 7625 7626 node.wrap(self.filterNode(new Node('ul', 1))); 7627 continue; 7628 } 7629 7630 // Try wrapping the element in a DIV 7631 if (schema.isValidChild(node.parent.name, 'div') && schema.isValidChild('div', node.name)) { 7632 node.wrap(self.filterNode(new Node('div', 1))); 7633 } else { 7634 // We failed wrapping it, then remove or unwrap it 7635 if (node.name === 'style' || node.name === 'script') { 7636 node.empty().remove(); 7637 } else { 7638 node.unwrap(); 7639 } 7640 } 7641 } 7642 } 7643 } 7644 7645 /** 7646 * Runs the specified node though the element and attributes filters. 7647 * 7648 * @method filterNode 7649 * @param {tinymce.html.Node} Node the node to run filters on. 7650 * @return {tinymce.html.Node} The passed in node. 7651 */ 7652 self.filterNode = function(node) { 7653 var i, name, list; 7654 7655 // Run element filters 7656 if (name in nodeFilters) { 7657 list = matchedNodes[name]; 7658 7659 if (list) { 7660 list.push(node); 7661 } else { 7662 matchedNodes[name] = [node]; 7663 } 7664 } 7665 7666 // Run attribute filters 7667 i = attributeFilters.length; 7668 while (i--) { 7669 name = attributeFilters[i].name; 7670 7671 if (name in node.attributes.map) { 7672 list = matchedAttributes[name]; 7673 7674 if (list) { 7675 list.push(node); 7676 } else { 7677 matchedAttributes[name] = [node]; 7678 } 7679 } 7680 } 7681 7682 return node; 7683 }; 7684 7685 /** 7686 * Adds a node filter function to the parser, the parser will collect the specified nodes by name 7687 * and then execute the callback ones it has finished parsing the document. 7688 * 7689 * @example 7690 * parser.addNodeFilter('p,h1', function(nodes, name) { 7691 * for (var i = 0; i < nodes.length; i++) { 7692 * console.log(nodes[i].name); 7693 * } 7694 * }); 7695 * @method addNodeFilter 7696 * @method {String} name Comma separated list of nodes to collect. 7697 * @param {function} callback Callback function to execute once it has collected nodes. 7698 */ 7699 self.addNodeFilter = function(name, callback) { 7700 each(explode(name), function(name) { 7701 var list = nodeFilters[name]; 7702 7703 if (!list) { 7704 nodeFilters[name] = list = []; 7705 } 7706 7707 list.push(callback); 7708 }); 7709 }; 7710 7711 /** 7712 * Adds a attribute filter function to the parser, the parser will collect nodes that has the specified attributes 7713 * and then execute the callback ones it has finished parsing the document. 7714 * 7715 * @example 7716 * parser.addAttributeFilter('src,href', function(nodes, name) { 7717 * for (var i = 0; i < nodes.length; i++) { 7718 * console.log(nodes[i].name); 7719 * } 7720 * }); 7721 * @method addAttributeFilter 7722 * @method {String} name Comma separated list of nodes to collect. 7723 * @param {function} callback Callback function to execute once it has collected nodes. 7724 */ 7725 self.addAttributeFilter = function(name, callback) { 7726 each(explode(name), function(name) { 7727 var i; 7728 7729 for (i = 0; i < attributeFilters.length; i++) { 7730 if (attributeFilters[i].name === name) { 7731 attributeFilters[i].callbacks.push(callback); 7732 return; 7733 } 7734 } 7735 7736 attributeFilters.push({name: name, callbacks: [callback]}); 7737 }); 7738 }; 7739 7740 /** 7741 * Parses the specified HTML string into a DOM like node tree and returns the result. 7742 * 7743 * @example 7744 * var rootNode = new DomParser({...}).parse('<b>text</b>'); 7745 * @method parse 7746 * @param {String} html Html string to sax parse. 7747 * @param {Object} args Optional args object that gets passed to all filter functions. 7748 * @return {tinymce.html.Node} Root node containing the tree. 7749 */ 7750 self.parse = function(html, args) { 7751 var parser, rootNode, node, nodes, i, l, fi, fl, list, name, validate; 7752 var blockElements, startWhiteSpaceRegExp, invalidChildren = [], isInWhiteSpacePreservedElement; 7753 var endWhiteSpaceRegExp, allWhiteSpaceRegExp, isAllWhiteSpaceRegExp, whiteSpaceElements; 7754 var children, nonEmptyElements, rootBlockName; 7755 7756 args = args || {}; 7757 matchedNodes = {}; 7758 matchedAttributes = {}; 7759 blockElements = extend(makeMap('script,style,head,html,body,title,meta,param'), schema.getBlockElements()); 7760 nonEmptyElements = schema.getNonEmptyElements(); 7761 children = schema.children; 7762 validate = settings.validate; 7763 rootBlockName = "forced_root_block" in args ? args.forced_root_block : settings.forced_root_block; 7764 7765 whiteSpaceElements = schema.getWhiteSpaceElements(); 7766 startWhiteSpaceRegExp = /^[ \t\r\n]+/; 7767 endWhiteSpaceRegExp = /[ \t\r\n]+$/; 7768 allWhiteSpaceRegExp = /[ \t\r\n]+/g; 7769 isAllWhiteSpaceRegExp = /^[ \t\r\n]+$/; 7770 7771 function addRootBlocks() { 7772 var node = rootNode.firstChild, next, rootBlockNode; 7773 7774 // Removes whitespace at beginning and end of block so: 7775 // <p> x </p> -> <p>x</p> 7776 function trim(rootBlockNode) { 7777 if (rootBlockNode) { 7778 node = rootBlockNode.firstChild; 7779 if (node && node.type == 3) { 7780 node.value = node.value.replace(startWhiteSpaceRegExp, ''); 7781 } 7782 7783 node = rootBlockNode.lastChild; 7784 if (node && node.type == 3) { 7785 node.value = node.value.replace(endWhiteSpaceRegExp, ''); 7786 } 7787 } 7788 } 7789 7790 // Check if rootBlock is valid within rootNode for example if P is valid in H1 if H1 is the contentEditabe root 7791 if (!schema.isValidChild(rootNode.name, rootBlockName.toLowerCase())) { 7792 return; 7793 } 7794 7795 while (node) { 7796 next = node.next; 7797 7798 if (node.type == 3 || (node.type == 1 && node.name !== 'p' && 7799 !blockElements[node.name] && !node.attr('data-mce-type'))) { 7800 if (!rootBlockNode) { 7801 // Create a new root block element 7802 rootBlockNode = createNode(rootBlockName, 1); 7803 rootBlockNode.attr(settings.forced_root_block_attrs); 7804 rootNode.insert(rootBlockNode, node); 7805 rootBlockNode.append(node); 7806 } else { 7807 rootBlockNode.append(node); 7808 } 7809 } else { 7810 trim(rootBlockNode); 7811 rootBlockNode = null; 7812 } 7813 7814 node = next; 7815 } 7816 7817 trim(rootBlockNode); 7818 } 7819 7820 function createNode(name, type) { 7821 var node = new Node(name, type), list; 7822 7823 if (name in nodeFilters) { 7824 list = matchedNodes[name]; 7825 7826 if (list) { 7827 list.push(node); 7828 } else { 7829 matchedNodes[name] = [node]; 7830 } 7831 } 7832 7833 return node; 7834 } 7835 7836 function removeWhitespaceBefore(node) { 7837 var textNode, textVal, sibling; 7838 7839 for (textNode = node.prev; textNode && textNode.type === 3; ) { 7840 textVal = textNode.value.replace(endWhiteSpaceRegExp, ''); 7841 7842 if (textVal.length > 0) { 7843 textNode.value = textVal; 7844 textNode = textNode.prev; 7845 } else { 7846 sibling = textNode.prev; 7847 textNode.remove(); 7848 textNode = sibling; 7849 } 7850 } 7851 } 7852 7853 function cloneAndExcludeBlocks(input) { 7854 var name, output = {}; 7855 7856 for (name in input) { 7857 if (name !== 'li' && name != 'p') { 7858 output[name] = input[name]; 7859 } 7860 } 7861 7862 return output; 7863 } 7864 7865 parser = new SaxParser({ 7866 validate: validate, 7867 allow_script_urls: settings.allow_script_urls, 7868 allow_conditional_comments: settings.allow_conditional_comments, 7869 7870 // Exclude P and LI from DOM parsing since it's treated better by the DOM parser 7871 self_closing_elements: cloneAndExcludeBlocks(schema.getSelfClosingElements()), 7872 7873 cdata: function(text) { 7874 node.append(createNode('#cdata', 4)).value = text; 7875 }, 7876 7877 text: function(text, raw) { 7878 var textNode; 7879 7880 // Trim all redundant whitespace on non white space elements 7881 if (!isInWhiteSpacePreservedElement) { 7882 text = text.replace(allWhiteSpaceRegExp, ' '); 7883 7884 if (node.lastChild && blockElements[node.lastChild.name]) { 7885 text = text.replace(startWhiteSpaceRegExp, ''); 7886 } 7887 } 7888 7889 // Do we need to create the node 7890 if (text.length !== 0) { 7891 textNode = createNode('#text', 3); 7892 textNode.raw = !!raw; 7893 node.append(textNode).value = text; 7894 } 7895 }, 7896 7897 comment: function(text) { 7898 node.append(createNode('#comment', 8)).value = text; 7899 }, 7900 7901 pi: function(name, text) { 7902 node.append(createNode(name, 7)).value = text; 7903 removeWhitespaceBefore(node); 7904 }, 7905 7906 doctype: function(text) { 7907 var newNode; 7908 7909 newNode = node.append(createNode('#doctype', 10)); 7910 newNode.value = text; 7911 removeWhitespaceBefore(node); 7912 }, 7913 7914 start: function(name, attrs, empty) { 7915 var newNode, attrFiltersLen, elementRule, attrName, parent; 7916 7917 elementRule = validate ? schema.getElementRule(name) : {}; 7918 if (elementRule) { 7919 newNode = createNode(elementRule.outputName || name, 1); 7920 newNode.attributes = attrs; 7921 newNode.shortEnded = empty; 7922 7923 node.append(newNode); 7924 7925 // Check if node is valid child of the parent node is the child is 7926 // unknown we don't collect it since it's probably a custom element 7927 parent = children[node.name]; 7928 if (parent && children[newNode.name] && !parent[newNode.name]) { 7929 invalidChildren.push(newNode); 7930 } 7931 7932 attrFiltersLen = attributeFilters.length; 7933 while (attrFiltersLen--) { 7934 attrName = attributeFilters[attrFiltersLen].name; 7935 7936 if (attrName in attrs.map) { 7937 list = matchedAttributes[attrName]; 7938 7939 if (list) { 7940 list.push(newNode); 7941 } else { 7942 matchedAttributes[attrName] = [newNode]; 7943 } 7944 } 7945 } 7946 7947 // Trim whitespace before block 7948 if (blockElements[name]) { 7949 removeWhitespaceBefore(newNode); 7950 } 7951 7952 // Change current node if the element wasn't empty i.e not <br /> or <img /> 7953 if (!empty) { 7954 node = newNode; 7955 } 7956 7957 // Check if we are inside a whitespace preserved element 7958 if (!isInWhiteSpacePreservedElement && whiteSpaceElements[name]) { 7959 isInWhiteSpacePreservedElement = true; 7960 } 7961 } 7962 }, 7963 7964 end: function(name) { 7965 var textNode, elementRule, text, sibling, tempNode; 7966 7967 elementRule = validate ? schema.getElementRule(name) : {}; 7968 if (elementRule) { 7969 if (blockElements[name]) { 7970 if (!isInWhiteSpacePreservedElement) { 7971 // Trim whitespace of the first node in a block 7972 textNode = node.firstChild; 7973 if (textNode && textNode.type === 3) { 7974 text = textNode.value.replace(startWhiteSpaceRegExp, ''); 7975 7976 // Any characters left after trim or should we remove it 7977 if (text.length > 0) { 7978 textNode.value = text; 7979 textNode = textNode.next; 7980 } else { 7981 sibling = textNode.next; 7982 textNode.remove(); 7983 textNode = sibling; 7984 7985 // Remove any pure whitespace siblings 7986 while (textNode && textNode.type === 3) { 7987 text = textNode.value; 7988 sibling = textNode.next; 7989 7990 if (text.length === 0 || isAllWhiteSpaceRegExp.test(text)) { 7991 textNode.remove(); 7992 textNode = sibling; 7993 } 7994 7995 textNode = sibling; 7996 } 7997 } 7998 } 7999 8000 // Trim whitespace of the last node in a block 8001 textNode = node.lastChild; 8002 if (textNode && textNode.type === 3) { 8003 text = textNode.value.replace(endWhiteSpaceRegExp, ''); 8004 8005 // Any characters left after trim or should we remove it 8006 if (text.length > 0) { 8007 textNode.value = text; 8008 textNode = textNode.prev; 8009 } else { 8010 sibling = textNode.prev; 8011 textNode.remove(); 8012 textNode = sibling; 8013 8014 // Remove any pure whitespace siblings 8015 while (textNode && textNode.type === 3) { 8016 text = textNode.value; 8017 sibling = textNode.prev; 8018 8019 if (text.length === 0 || isAllWhiteSpaceRegExp.test(text)) { 8020 textNode.remove(); 8021 textNode = sibling; 8022 } 8023 8024 textNode = sibling; 8025 } 8026 } 8027 } 8028 } 8029 8030 // Trim start white space 8031 // Removed due to: #5424 8032 /*textNode = node.prev; 8033 if (textNode && textNode.type === 3) { 8034 text = textNode.value.replace(startWhiteSpaceRegExp, ''); 8035 8036 if (text.length > 0) 8037 textNode.value = text; 8038 else 8039 textNode.remove(); 8040 }*/ 8041 } 8042 8043 // Check if we exited a whitespace preserved element 8044 if (isInWhiteSpacePreservedElement && whiteSpaceElements[name]) { 8045 isInWhiteSpacePreservedElement = false; 8046 } 8047 8048 // Handle empty nodes 8049 if (elementRule.removeEmpty || elementRule.paddEmpty) { 8050 if (node.isEmpty(nonEmptyElements)) { 8051 if (elementRule.paddEmpty) { 8052 node.empty().append(new Node('#text', '3')).value = '\u00a0'; 8053 } else { 8054 // Leave nodes that have a name like <a name="name"> 8055 if (!node.attributes.map.name && !node.attributes.map.id) { 8056 tempNode = node.parent; 8057 node.empty().remove(); 8058 node = tempNode; 8059 return; 8060 } 8061 } 8062 } 8063 } 8064 8065 node = node.parent; 8066 } 8067 } 8068 }, schema); 8069 8070 rootNode = node = new Node(args.context || settings.root_name, 11); 8071 8072 parser.parse(html); 8073 8074 // Fix invalid children or report invalid children in a contextual parsing 8075 if (validate && invalidChildren.length) { 8076 if (!args.context) { 8077 fixInvalidChildren(invalidChildren); 8078 } else { 8079 args.invalid = true; 8080 } 8081 } 8082 8083 // Wrap nodes in the root into block elements if the root is body 8084 if (rootBlockName && (rootNode.name == 'body' || args.isRootContent)) { 8085 addRootBlocks(); 8086 } 8087 8088 // Run filters only when the contents is valid 8089 if (!args.invalid) { 8090 // Run node filters 8091 for (name in matchedNodes) { 8092 list = nodeFilters[name]; 8093 nodes = matchedNodes[name]; 8094 8095 // Remove already removed children 8096 fi = nodes.length; 8097 while (fi--) { 8098 if (!nodes[fi].parent) { 8099 nodes.splice(fi, 1); 8100 } 8101 } 8102 8103 for (i = 0, l = list.length; i < l; i++) { 8104 list[i](nodes, name, args); 8105 } 8106 } 8107 8108 // Run attribute filters 8109 for (i = 0, l = attributeFilters.length; i < l; i++) { 8110 list = attributeFilters[i]; 8111 8112 if (list.name in matchedAttributes) { 8113 nodes = matchedAttributes[list.name]; 8114 8115 // Remove already removed children 8116 fi = nodes.length; 8117 while (fi--) { 8118 if (!nodes[fi].parent) { 8119 nodes.splice(fi, 1); 8120 } 8121 } 8122 8123 for (fi = 0, fl = list.callbacks.length; fi < fl; fi++) { 8124 list.callbacks[fi](nodes, list.name, args); 8125 } 8126 } 8127 } 8128 } 8129 8130 return rootNode; 8131 }; 8132 8133 // Remove <br> at end of block elements Gecko and WebKit injects BR elements to 8134 // make it possible to place the caret inside empty blocks. This logic tries to remove 8135 // these elements and keep br elements that where intended to be there intact 8136 if (settings.remove_trailing_brs) { 8137 self.addNodeFilter('br', function(nodes) { 8138 var i, l = nodes.length, node, blockElements = extend({}, schema.getBlockElements()); 8139 var nonEmptyElements = schema.getNonEmptyElements(), parent, lastParent, prev, prevName; 8140 var elementRule, textNode; 8141 8142 // Remove brs from body element as well 8143 blockElements.body = 1; 8144 8145 // Must loop forwards since it will otherwise remove all brs in <p>a<br><br><br></p> 8146 for (i = 0; i < l; i++) { 8147 node = nodes[i]; 8148 parent = node.parent; 8149 8150 if (blockElements[node.parent.name] && node === parent.lastChild) { 8151 // Loop all nodes to the left of the current node and check for other BR elements 8152 // excluding bookmarks since they are invisible 8153 prev = node.prev; 8154 while (prev) { 8155 prevName = prev.name; 8156 8157 // Ignore bookmarks 8158 if (prevName !== "span" || prev.attr('data-mce-type') !== 'bookmark') { 8159 // Found a non BR element 8160 if (prevName !== "br") { 8161 break; 8162 } 8163 8164 // Found another br it's a <br><br> structure then don't remove anything 8165 if (prevName === 'br') { 8166 node = null; 8167 break; 8168 } 8169 } 8170 8171 prev = prev.prev; 8172 } 8173 8174 if (node) { 8175 node.remove(); 8176 8177 // Is the parent to be considered empty after we removed the BR 8178 if (parent.isEmpty(nonEmptyElements)) { 8179 elementRule = schema.getElementRule(parent.name); 8180 8181 // Remove or padd the element depending on schema rule 8182 if (elementRule) { 8183 if (elementRule.removeEmpty) { 8184 parent.remove(); 8185 } else if (elementRule.paddEmpty) { 8186 parent.empty().append(new Node('#text', 3)).value = '\u00a0'; 8187 } 8188 } 8189 } 8190 } 8191 } else { 8192 // Replaces BR elements inside inline elements like <p><b><i><br></i></b></p> 8193 // so they become <p><b><i> </i></b></p> 8194 lastParent = node; 8195 while (parent && parent.firstChild === lastParent && parent.lastChild === lastParent) { 8196 lastParent = parent; 8197 8198 if (blockElements[parent.name]) { 8199 break; 8200 } 8201 8202 parent = parent.parent; 8203 } 8204 8205 if (lastParent === parent) { 8206 textNode = new Node('#text', 3); 8207 textNode.value = '\u00a0'; 8208 node.replace(textNode); 8209 } 8210 } 8211 } 8212 }); 8213 } 8214 8215 // Force anchor names closed, unless the setting "allow_html_in_named_anchor" is explicitly included. 8216 if (!settings.allow_html_in_named_anchor) { 8217 self.addAttributeFilter('id,name', function(nodes) { 8218 var i = nodes.length, sibling, prevSibling, parent, node; 8219 8220 while (i--) { 8221 node = nodes[i]; 8222 if (node.name === 'a' && node.firstChild && !node.attr('href')) { 8223 parent = node.parent; 8224 8225 // Move children after current node 8226 sibling = node.lastChild; 8227 do { 8228 prevSibling = sibling.prev; 8229 parent.insert(sibling, node); 8230 sibling = prevSibling; 8231 } while (sibling); 8232 } 8233 } 8234 }); 8235 } 8236 }; 8237 }); 8238 8239 // Included from: js/tinymce/classes/html/Writer.js 8240 8241 /** 8242 * Writer.js 8243 * 8244 * Copyright, Moxiecode Systems AB 8245 * Released under LGPL License. 8246 * 8247 * License: http://www.tinymce.com/license 8248 * Contributing: http://www.tinymce.com/contributing 8249 */ 8250 8251 /** 8252 * This class is used to write HTML tags out it can be used with the Serializer or the SaxParser. 8253 * 8254 * @class tinymce.html.Writer 8255 * @example 8256 * var writer = new tinymce.html.Writer({indent: true}); 8257 * var parser = new tinymce.html.SaxParser(writer).parse('<p><br></p>'); 8258 * console.log(writer.getContent()); 8259 * 8260 * @class tinymce.html.Writer 8261 * @version 3.4 8262 */ 8263 define("tinymce/html/Writer", [ 8264 "tinymce/html/Entities", 8265 "tinymce/util/Tools" 8266 ], function(Entities, Tools) { 8267 var makeMap = Tools.makeMap; 8268 8269 /** 8270 * Constructs a new Writer instance. 8271 * 8272 * @constructor 8273 * @method Writer 8274 * @param {Object} settings Name/value settings object. 8275 */ 8276 return function(settings) { 8277 var html = [], indent, indentBefore, indentAfter, encode, htmlOutput; 8278 8279 settings = settings || {}; 8280 indent = settings.indent; 8281 indentBefore = makeMap(settings.indent_before || ''); 8282 indentAfter = makeMap(settings.indent_after || ''); 8283 encode = Entities.getEncodeFunc(settings.entity_encoding || 'raw', settings.entities); 8284 htmlOutput = settings.element_format == "html"; 8285 8286 return { 8287 /** 8288 * Writes the a start element such as <p id="a">. 8289 * 8290 * @method start 8291 * @param {String} name Name of the element. 8292 * @param {Array} attrs Optional attribute array or undefined if it hasn't any. 8293 * @param {Boolean} empty Optional empty state if the tag should end like <br />. 8294 */ 8295 start: function(name, attrs, empty) { 8296 var i, l, attr, value; 8297 8298 if (indent && indentBefore[name] && html.length > 0) { 8299 value = html[html.length - 1]; 8300 8301 if (value.length > 0 && value !== '\n') { 8302 html.push('\n'); 8303 } 8304 } 8305 8306 html.push('<', name); 8307 8308 if (attrs) { 8309 for (i = 0, l = attrs.length; i < l; i++) { 8310 attr = attrs[i]; 8311 html.push(' ', attr.name, '="', encode(attr.value, true), '"'); 8312 } 8313 } 8314 8315 if (!empty || htmlOutput) { 8316 html[html.length] = '>'; 8317 } else { 8318 html[html.length] = ' />'; 8319 } 8320 8321 if (empty && indent && indentAfter[name] && html.length > 0) { 8322 value = html[html.length - 1]; 8323 8324 if (value.length > 0 && value !== '\n') { 8325 html.push('\n'); 8326 } 8327 } 8328 }, 8329 8330 /** 8331 * Writes the a end element such as </p>. 8332 * 8333 * @method end 8334 * @param {String} name Name of the element. 8335 */ 8336 end: function(name) { 8337 var value; 8338 8339 /*if (indent && indentBefore[name] && html.length > 0) { 8340 value = html[html.length - 1]; 8341 8342 if (value.length > 0 && value !== '\n') 8343 html.push('\n'); 8344 }*/ 8345 8346 html.push('</', name, '>'); 8347 8348 if (indent && indentAfter[name] && html.length > 0) { 8349 value = html[html.length - 1]; 8350 8351 if (value.length > 0 && value !== '\n') { 8352 html.push('\n'); 8353 } 8354 } 8355 }, 8356 8357 /** 8358 * Writes a text node. 8359 * 8360 * @method text 8361 * @param {String} text String to write out. 8362 * @param {Boolean} raw Optional raw state if true the contents wont get encoded. 8363 */ 8364 text: function(text, raw) { 8365 if (text.length > 0) { 8366 html[html.length] = raw ? text : encode(text); 8367 } 8368 }, 8369 8370 /** 8371 * Writes a cdata node such as <![CDATA[data]]>. 8372 * 8373 * @method cdata 8374 * @param {String} text String to write out inside the cdata. 8375 */ 8376 cdata: function(text) { 8377 html.push('<![CDATA[', text, ']]>'); 8378 }, 8379 8380 /** 8381 * Writes a comment node such as <!-- Comment -->. 8382 * 8383 * @method cdata 8384 * @param {String} text String to write out inside the comment. 8385 */ 8386 comment: function(text) { 8387 html.push('<!--', text, '-->'); 8388 }, 8389 8390 /** 8391 * Writes a PI node such as <?xml attr="value" ?>. 8392 * 8393 * @method pi 8394 * @param {String} name Name of the pi. 8395 * @param {String} text String to write out inside the pi. 8396 */ 8397 pi: function(name, text) { 8398 if (text) { 8399 html.push('<?', name, ' ', text, '?>'); 8400 } else { 8401 html.push('<?', name, '?>'); 8402 } 8403 8404 if (indent) { 8405 html.push('\n'); 8406 } 8407 }, 8408 8409 /** 8410 * Writes a doctype node such as <!DOCTYPE data>. 8411 * 8412 * @method doctype 8413 * @param {String} text String to write out inside the doctype. 8414 */ 8415 doctype: function(text) { 8416 html.push('<!DOCTYPE', text, '>', indent ? '\n' : ''); 8417 }, 8418 8419 /** 8420 * Resets the internal buffer if one wants to reuse the writer. 8421 * 8422 * @method reset 8423 */ 8424 reset: function() { 8425 html.length = 0; 8426 }, 8427 8428 /** 8429 * Returns the contents that got serialized. 8430 * 8431 * @method getContent 8432 * @return {String} HTML contents that got written down. 8433 */ 8434 getContent: function() { 8435 return html.join('').replace(/\n$/, ''); 8436 } 8437 }; 8438 }; 8439 }); 8440 8441 // Included from: js/tinymce/classes/html/Serializer.js 8442 8443 /** 8444 * Serializer.js 8445 * 8446 * Copyright, Moxiecode Systems AB 8447 * Released under LGPL License. 8448 * 8449 * License: http://www.tinymce.com/license 8450 * Contributing: http://www.tinymce.com/contributing 8451 */ 8452 8453 /** 8454 * This class is used to serialize down the DOM tree into a string using a Writer instance. 8455 * 8456 * 8457 * @example 8458 * new tinymce.html.Serializer().serialize(new tinymce.html.DomParser().parse('<p>text</p>')); 8459 * @class tinymce.html.Serializer 8460 * @version 3.4 8461 */ 8462 define("tinymce/html/Serializer", [ 8463 "tinymce/html/Writer", 8464 "tinymce/html/Schema" 8465 ], function(Writer, Schema) { 8466 /** 8467 * Constructs a new Serializer instance. 8468 * 8469 * @constructor 8470 * @method Serializer 8471 * @param {Object} settings Name/value settings object. 8472 * @param {tinymce.html.Schema} schema Schema instance to use. 8473 */ 8474 return function(settings, schema) { 8475 var self = this, writer = new Writer(settings); 8476 8477 settings = settings || {}; 8478 settings.validate = "validate" in settings ? settings.validate : true; 8479 8480 self.schema = schema = schema || new Schema(); 8481 self.writer = writer; 8482 8483 /** 8484 * Serializes the specified node into a string. 8485 * 8486 * @example 8487 * new tinymce.html.Serializer().serialize(new tinymce.html.DomParser().parse('<p>text</p>')); 8488 * @method serialize 8489 * @param {tinymce.html.Node} node Node instance to serialize. 8490 * @return {String} String with HTML based on DOM tree. 8491 */ 8492 self.serialize = function(node) { 8493 var handlers, validate; 8494 8495 validate = settings.validate; 8496 8497 handlers = { 8498 // #text 8499 3: function(node) { 8500 writer.text(node.value, node.raw); 8501 }, 8502 8503 // #comment 8504 8: function(node) { 8505 writer.comment(node.value); 8506 }, 8507 8508 // Processing instruction 8509 7: function(node) { 8510 writer.pi(node.name, node.value); 8511 }, 8512 8513 // Doctype 8514 10: function(node) { 8515 writer.doctype(node.value); 8516 }, 8517 8518 // CDATA 8519 4: function(node) { 8520 writer.cdata(node.value); 8521 }, 8522 8523 // Document fragment 8524 11: function(node) { 8525 if ((node = node.firstChild)) { 8526 do { 8527 walk(node); 8528 } while ((node = node.next)); 8529 } 8530 } 8531 }; 8532 8533 writer.reset(); 8534 8535 function walk(node) { 8536 var handler = handlers[node.type], name, isEmpty, attrs, attrName, attrValue, sortedAttrs, i, l, elementRule; 8537 8538 if (!handler) { 8539 name = node.name; 8540 isEmpty = node.shortEnded; 8541 attrs = node.attributes; 8542 8543 // Sort attributes 8544 if (validate && attrs && attrs.length > 1) { 8545 sortedAttrs = []; 8546 sortedAttrs.map = {}; 8547 8548 elementRule = schema.getElementRule(node.name); 8549 for (i = 0, l = elementRule.attributesOrder.length; i < l; i++) { 8550 attrName = elementRule.attributesOrder[i]; 8551 8552 if (attrName in attrs.map) { 8553 attrValue = attrs.map[attrName]; 8554 sortedAttrs.map[attrName] = attrValue; 8555 sortedAttrs.push({name: attrName, value: attrValue}); 8556 } 8557 } 8558 8559 for (i = 0, l = attrs.length; i < l; i++) { 8560 attrName = attrs[i].name; 8561 8562 if (!(attrName in sortedAttrs.map)) { 8563 attrValue = attrs.map[attrName]; 8564 sortedAttrs.map[attrName] = attrValue; 8565 sortedAttrs.push({name: attrName, value: attrValue}); 8566 } 8567 } 8568 8569 attrs = sortedAttrs; 8570 } 8571 8572 writer.start(node.name, attrs, isEmpty); 8573 8574 if (!isEmpty) { 8575 if ((node = node.firstChild)) { 8576 do { 8577 walk(node); 8578 } while ((node = node.next)); 8579 } 8580 8581 writer.end(name); 8582 } 8583 } else { 8584 handler(node); 8585 } 8586 } 8587 8588 // Serialize element and treat all non elements as fragments 8589 if (node.type == 1 && !settings.inner) { 8590 walk(node); 8591 } else { 8592 handlers[11](node); 8593 } 8594 8595 return writer.getContent(); 8596 }; 8597 }; 8598 }); 8599 8600 // Included from: js/tinymce/classes/dom/Serializer.js 8601 8602 /** 8603 * Serializer.js 8604 * 8605 * Copyright, Moxiecode Systems AB 8606 * Released under LGPL License. 8607 * 8608 * License: http://www.tinymce.com/license 8609 * Contributing: http://www.tinymce.com/contributing 8610 */ 8611 8612 /** 8613 * This class is used to serialize DOM trees into a string. Consult the TinyMCE Wiki API for 8614 * more details and examples on how to use this class. 8615 * 8616 * @class tinymce.dom.Serializer 8617 */ 8618 define("tinymce/dom/Serializer", [ 8619 "tinymce/dom/DOMUtils", 8620 "tinymce/html/DomParser", 8621 "tinymce/html/Entities", 8622 "tinymce/html/Serializer", 8623 "tinymce/html/Node", 8624 "tinymce/html/Schema", 8625 "tinymce/Env", 8626 "tinymce/util/Tools" 8627 ], function(DOMUtils, DomParser, Entities, Serializer, Node, Schema, Env, Tools) { 8628 var each = Tools.each, trim = Tools.trim; 8629 var DOM = DOMUtils.DOM; 8630 8631 /** 8632 * Constructs a new DOM serializer class. 8633 * 8634 * @constructor 8635 * @method Serializer 8636 * @param {Object} settings Serializer settings object. 8637 * @param {tinymce.Editor} editor Optional editor to bind events to and get schema/dom from. 8638 */ 8639 return function(settings, editor) { 8640 var dom, schema, htmlParser; 8641 8642 if (editor) { 8643 dom = editor.dom; 8644 schema = editor.schema; 8645 } 8646 8647 // Default DOM and Schema if they are undefined 8648 dom = dom || DOM; 8649 schema = schema || new Schema(settings); 8650 settings.entity_encoding = settings.entity_encoding || 'named'; 8651 settings.remove_trailing_brs = "remove_trailing_brs" in settings ? settings.remove_trailing_brs : true; 8652 8653 htmlParser = new DomParser(settings, schema); 8654 8655 // Convert tabindex back to elements when serializing contents 8656 htmlParser.addAttributeFilter('data-mce-tabindex', function(nodes, name) { 8657 var i = nodes.length, node; 8658 8659 while (i--) { 8660 node = nodes[i]; 8661 node.attr('tabindex', node.attributes.map['data-mce-tabindex']); 8662 node.attr(name, null); 8663 } 8664 }); 8665 8666 // Convert move data-mce-src, data-mce-href and data-mce-style into nodes or process them if needed 8667 htmlParser.addAttributeFilter('src,href,style', function(nodes, name) { 8668 var i = nodes.length, node, value, internalName = 'data-mce-' + name; 8669 var urlConverter = settings.url_converter, urlConverterScope = settings.url_converter_scope, undef; 8670 8671 while (i--) { 8672 node = nodes[i]; 8673 8674 value = node.attributes.map[internalName]; 8675 if (value !== undef) { 8676 // Set external name to internal value and remove internal 8677 node.attr(name, value.length > 0 ? value : null); 8678 node.attr(internalName, null); 8679 } else { 8680 // No internal attribute found then convert the value we have in the DOM 8681 value = node.attributes.map[name]; 8682 8683 if (name === "style") { 8684 value = dom.serializeStyle(dom.parseStyle(value), node.name); 8685 } else if (urlConverter) { 8686 value = urlConverter.call(urlConverterScope, value, name, node.name); 8687 } 8688 8689 node.attr(name, value.length > 0 ? value : null); 8690 } 8691 } 8692 }); 8693 8694 // Remove internal classes mceItem<..> or mceSelected 8695 htmlParser.addAttributeFilter('class', function(nodes) { 8696 var i = nodes.length, node, value; 8697 8698 while (i--) { 8699 node = nodes[i]; 8700 value = node.attr('class').replace(/(?:^|\s)mce-item-\w+(?!\S)/g, ''); 8701 node.attr('class', value.length > 0 ? value : null); 8702 } 8703 }); 8704 8705 // Remove bookmark elements 8706 htmlParser.addAttributeFilter('data-mce-type', function(nodes, name, args) { 8707 var i = nodes.length, node; 8708 8709 while (i--) { 8710 node = nodes[i]; 8711 8712 if (node.attributes.map['data-mce-type'] === 'bookmark' && !args.cleanup) { 8713 node.remove(); 8714 } 8715 } 8716 }); 8717 8718 // Remove expando attributes 8719 htmlParser.addAttributeFilter('data-mce-expando', function(nodes, name) { 8720 var i = nodes.length; 8721 8722 while (i--) { 8723 nodes[i].attr(name, null); 8724 } 8725 }); 8726 8727 htmlParser.addNodeFilter('noscript', function(nodes) { 8728 var i = nodes.length, node; 8729 8730 while (i--) { 8731 node = nodes[i].firstChild; 8732 8733 if (node) { 8734 node.value = Entities.decode(node.value); 8735 } 8736 } 8737 }); 8738 8739 // Force script into CDATA sections and remove the mce- prefix also add comments around styles 8740 htmlParser.addNodeFilter('script,style', function(nodes, name) { 8741 var i = nodes.length, node, value; 8742 8743 function trim(value) { 8744 /*jshint maxlen:255 */ 8745 /*eslint max-len:0 */ 8746 return value.replace(/(<!--\[CDATA\[|\]\]-->)/g, '\n') 8747 .replace(/^[\r\n]*|[\r\n]*$/g, '') 8748 .replace(/^\s*((<!--)?(\s*\/\/)?\s*<!\[CDATA\[|(<!--\s*)?\/\*\s*<!\[CDATA\[\s*\*\/|(\/\/)?\s*<!--|\/\*\s*<!--\s*\*\/)\s*[\r\n]*/gi, '') 8749 .replace(/\s*(\/\*\s*\]\]>\s*\*\/(-->)?|\s*\/\/\s*\]\]>(-->)?|\/\/\s*(-->)?|\]\]>|\/\*\s*-->\s*\*\/|\s*-->\s*)\s*$/g, ''); 8750 } 8751 8752 while (i--) { 8753 node = nodes[i]; 8754 value = node.firstChild ? node.firstChild.value : ''; 8755 8756 if (name === "script") { 8757 // Remove mce- prefix from script elements and remove default text/javascript mime type (HTML5) 8758 var type = (node.attr('type') || 'text/javascript').replace(/^mce\-/, ''); 8759 node.attr('type', type === 'text/javascript' ? null : type); 8760 8761 if (value.length > 0) { 8762 node.firstChild.value = '// <![CDATA[\n' + trim(value) + '\n// ]]>'; 8763 } 8764 } else { 8765 if (value.length > 0) { 8766 node.firstChild.value = '<!--\n' + trim(value) + '\n-->'; 8767 } 8768 } 8769 } 8770 }); 8771 8772 // Convert comments to cdata and handle protected comments 8773 htmlParser.addNodeFilter('#comment', function(nodes) { 8774 var i = nodes.length, node; 8775 8776 while (i--) { 8777 node = nodes[i]; 8778 8779 if (node.value.indexOf('[CDATA[') === 0) { 8780 node.name = '#cdata'; 8781 node.type = 4; 8782 node.value = node.value.replace(/^\[CDATA\[|\]\]$/g, ''); 8783 } else if (node.value.indexOf('mce:protected ') === 0) { 8784 node.name = "#text"; 8785 node.type = 3; 8786 node.raw = true; 8787 node.value = unescape(node.value).substr(14); 8788 } 8789 } 8790 }); 8791 8792 htmlParser.addNodeFilter('xml:namespace,input', function(nodes, name) { 8793 var i = nodes.length, node; 8794 8795 while (i--) { 8796 node = nodes[i]; 8797 if (node.type === 7) { 8798 node.remove(); 8799 } else if (node.type === 1) { 8800 if (name === "input" && !("type" in node.attributes.map)) { 8801 node.attr('type', 'text'); 8802 } 8803 } 8804 } 8805 }); 8806 8807 // Fix list elements, TODO: Replace this later 8808 if (settings.fix_list_elements) { 8809 htmlParser.addNodeFilter('ul,ol', function(nodes) { 8810 var i = nodes.length, node, parentNode; 8811 8812 while (i--) { 8813 node = nodes[i]; 8814 parentNode = node.parent; 8815 8816 if (parentNode.name === 'ul' || parentNode.name === 'ol') { 8817 if (node.prev && node.prev.name === 'li') { 8818 node.prev.append(node); 8819 } 8820 } 8821 } 8822 }); 8823 } 8824 8825 // Remove internal data attributes 8826 htmlParser.addAttributeFilter('data-mce-src,data-mce-href,data-mce-style,data-mce-selected', function(nodes, name) { 8827 var i = nodes.length; 8828 8829 while (i--) { 8830 nodes[i].attr(name, null); 8831 } 8832 }); 8833 8834 // Return public methods 8835 return { 8836 /** 8837 * Schema instance that was used to when the Serializer was constructed. 8838 * 8839 * @field {tinymce.html.Schema} schema 8840 */ 8841 schema: schema, 8842 8843 /** 8844 * Adds a node filter function to the parser used by the serializer, the parser will collect the specified nodes by name 8845 * and then execute the callback ones it has finished parsing the document. 8846 * 8847 * @example 8848 * parser.addNodeFilter('p,h1', function(nodes, name) { 8849 * for (var i = 0; i < nodes.length; i++) { 8850 * console.log(nodes[i].name); 8851 * } 8852 * }); 8853 * @method addNodeFilter 8854 * @method {String} name Comma separated list of nodes to collect. 8855 * @param {function} callback Callback function to execute once it has collected nodes. 8856 */ 8857 addNodeFilter: htmlParser.addNodeFilter, 8858 8859 /** 8860 * Adds a attribute filter function to the parser used by the serializer, the parser will 8861 * collect nodes that has the specified attributes 8862 * and then execute the callback ones it has finished parsing the document. 8863 * 8864 * @example 8865 * parser.addAttributeFilter('src,href', function(nodes, name) { 8866 * for (var i = 0; i < nodes.length; i++) { 8867 * console.log(nodes[i].name); 8868 * } 8869 * }); 8870 * @method addAttributeFilter 8871 * @method {String} name Comma separated list of nodes to collect. 8872 * @param {function} callback Callback function to execute once it has collected nodes. 8873 */ 8874 addAttributeFilter: htmlParser.addAttributeFilter, 8875 8876 /** 8877 * Serializes the specified browser DOM node into a HTML string. 8878 * 8879 * @method serialize 8880 * @param {DOMNode} node DOM node to serialize. 8881 * @param {Object} args Arguments option that gets passed to event handlers. 8882 */ 8883 serialize: function(node, args) { 8884 var self = this, impl, doc, oldDoc, htmlSerializer, content; 8885 8886 // Explorer won't clone contents of script and style and the 8887 // selected index of select elements are cleared on a clone operation. 8888 if (Env.ie && dom.select('script,style,select,map').length > 0) { 8889 content = node.innerHTML; 8890 node = node.cloneNode(false); 8891 dom.setHTML(node, content); 8892 } else { 8893 node = node.cloneNode(true); 8894 } 8895 8896 // Nodes needs to be attached to something in WebKit/Opera 8897 // This fix will make DOM ranges and make Sizzle happy! 8898 impl = node.ownerDocument.implementation; 8899 if (impl.createHTMLDocument) { 8900 // Create an empty HTML document 8901 doc = impl.createHTMLDocument(""); 8902 8903 // Add the element or it's children if it's a body element to the new document 8904 each(node.nodeName == 'BODY' ? node.childNodes : [node], function(node) { 8905 doc.body.appendChild(doc.importNode(node, true)); 8906 }); 8907 8908 // Grab first child or body element for serialization 8909 if (node.nodeName != 'BODY') { 8910 node = doc.body.firstChild; 8911 } else { 8912 node = doc.body; 8913 } 8914 8915 // set the new document in DOMUtils so createElement etc works 8916 oldDoc = dom.doc; 8917 dom.doc = doc; 8918 } 8919 8920 args = args || {}; 8921 args.format = args.format || 'html'; 8922 8923 // Don't wrap content if we want selected html 8924 if (args.selection) { 8925 args.forced_root_block = ''; 8926 } 8927 8928 // Pre process 8929 if (!args.no_events) { 8930 args.node = node; 8931 self.onPreProcess(args); 8932 } 8933 8934 // Setup serializer 8935 htmlSerializer = new Serializer(settings, schema); 8936 8937 // Parse and serialize HTML 8938 args.content = htmlSerializer.serialize( 8939 htmlParser.parse(trim(args.getInner ? node.innerHTML : dom.getOuterHTML(node)), args) 8940 ); 8941 8942 // Replace all BOM characters for now until we can find a better solution 8943 if (!args.cleanup) { 8944 args.content = args.content.replace(/\uFEFF/g, ''); 8945 } 8946 8947 // Post process 8948 if (!args.no_events) { 8949 self.onPostProcess(args); 8950 } 8951 8952 // Restore the old document if it was changed 8953 if (oldDoc) { 8954 dom.doc = oldDoc; 8955 } 8956 8957 args.node = null; 8958 8959 return args.content; 8960 }, 8961 8962 /** 8963 * Adds valid elements rules to the serializers schema instance this enables you to specify things 8964 * like what elements should be outputted and what attributes specific elements might have. 8965 * Consult the Wiki for more details on this format. 8966 * 8967 * @method addRules 8968 * @param {String} rules Valid elements rules string to add to schema. 8969 */ 8970 addRules: function(rules) { 8971 schema.addValidElements(rules); 8972 }, 8973 8974 /** 8975 * Sets the valid elements rules to the serializers schema instance this enables you to specify things 8976 * like what elements should be outputted and what attributes specific elements might have. 8977 * Consult the Wiki for more details on this format. 8978 * 8979 * @method setRules 8980 * @param {String} rules Valid elements rules string. 8981 */ 8982 setRules: function(rules) { 8983 schema.setValidElements(rules); 8984 }, 8985 8986 onPreProcess: function(args) { 8987 if (editor) { 8988 editor.fire('PreProcess', args); 8989 } 8990 }, 8991 8992 onPostProcess: function(args) { 8993 if (editor) { 8994 editor.fire('PostProcess', args); 8995 } 8996 } 8997 }; 8998 }; 8999 }); 9000 9001 // Included from: js/tinymce/classes/dom/TridentSelection.js 9002 9003 /** 9004 * TridentSelection.js 9005 * 9006 * Copyright, Moxiecode Systems AB 9007 * Released under LGPL License. 9008 * 9009 * License: http://www.tinymce.com/license 9010 * Contributing: http://www.tinymce.com/contributing 9011 */ 9012 9013 /** 9014 * Selection class for old explorer versions. This one fakes the 9015 * native selection object available on modern browsers. 9016 * 9017 * @class tinymce.dom.TridentSelection 9018 */ 9019 define("tinymce/dom/TridentSelection", [], function() { 9020 function Selection(selection) { 9021 var self = this, dom = selection.dom, FALSE = false; 9022 9023 function getPosition(rng, start) { 9024 var checkRng, startIndex = 0, endIndex, inside, 9025 children, child, offset, index, position = -1, parent; 9026 9027 // Setup test range, collapse it and get the parent 9028 checkRng = rng.duplicate(); 9029 checkRng.collapse(start); 9030 parent = checkRng.parentElement(); 9031 9032 // Check if the selection is within the right document 9033 if (parent.ownerDocument !== selection.dom.doc) { 9034 return; 9035 } 9036 9037 // IE will report non editable elements as it's parent so look for an editable one 9038 while (parent.contentEditable === "false") { 9039 parent = parent.parentNode; 9040 } 9041 9042 // If parent doesn't have any children then return that we are inside the element 9043 if (!parent.hasChildNodes()) { 9044 return {node: parent, inside: 1}; 9045 } 9046 9047 // Setup node list and endIndex 9048 children = parent.children; 9049 endIndex = children.length - 1; 9050 9051 // Perform a binary search for the position 9052 while (startIndex <= endIndex) { 9053 index = Math.floor((startIndex + endIndex) / 2); 9054 9055 // Move selection to node and compare the ranges 9056 child = children[index]; 9057 checkRng.moveToElementText(child); 9058 position = checkRng.compareEndPoints(start ? 'StartToStart' : 'EndToEnd', rng); 9059 9060 // Before/after or an exact match 9061 if (position > 0) { 9062 endIndex = index - 1; 9063 } else if (position < 0) { 9064 startIndex = index + 1; 9065 } else { 9066 return {node: child}; 9067 } 9068 } 9069 9070 // Check if child position is before or we didn't find a position 9071 if (position < 0) { 9072 // No element child was found use the parent element and the offset inside that 9073 if (!child) { 9074 checkRng.moveToElementText(parent); 9075 checkRng.collapse(true); 9076 child = parent; 9077 inside = true; 9078 } else { 9079 checkRng.collapse(false); 9080 } 9081 9082 // Walk character by character in text node until we hit the selected range endpoint, 9083 // hit the end of document or parent isn't the right one 9084 // We need to walk char by char since rng.text or rng.htmlText will trim line endings 9085 offset = 0; 9086 while (checkRng.compareEndPoints(start ? 'StartToStart' : 'StartToEnd', rng) !== 0) { 9087 if (checkRng.move('character', 1) === 0 || parent != checkRng.parentElement()) { 9088 break; 9089 } 9090 9091 offset++; 9092 } 9093 } else { 9094 // Child position is after the selection endpoint 9095 checkRng.collapse(true); 9096 9097 // Walk character by character in text node until we hit the selected range endpoint, hit 9098 // the end of document or parent isn't the right one 9099 offset = 0; 9100 while (checkRng.compareEndPoints(start ? 'StartToStart' : 'StartToEnd', rng) !== 0) { 9101 if (checkRng.move('character', -1) === 0 || parent != checkRng.parentElement()) { 9102 break; 9103 } 9104 9105 offset++; 9106 } 9107 } 9108 9109 return {node: child, position: position, offset: offset, inside: inside}; 9110 } 9111 9112 // Returns a W3C DOM compatible range object by using the IE Range API 9113 function getRange() { 9114 var ieRange = selection.getRng(), domRange = dom.createRng(), element, collapsed, tmpRange, element2, bookmark; 9115 9116 // If selection is outside the current document just return an empty range 9117 element = ieRange.item ? ieRange.item(0) : ieRange.parentElement(); 9118 if (element.ownerDocument != dom.doc) { 9119 return domRange; 9120 } 9121 9122 collapsed = selection.isCollapsed(); 9123 9124 // Handle control selection 9125 if (ieRange.item) { 9126 domRange.setStart(element.parentNode, dom.nodeIndex(element)); 9127 domRange.setEnd(domRange.startContainer, domRange.startOffset + 1); 9128 9129 return domRange; 9130 } 9131 9132 function findEndPoint(start) { 9133 var endPoint = getPosition(ieRange, start), container, offset, textNodeOffset = 0, sibling, undef, nodeValue; 9134 9135 container = endPoint.node; 9136 offset = endPoint.offset; 9137 9138 if (endPoint.inside && !container.hasChildNodes()) { 9139 domRange[start ? 'setStart' : 'setEnd'](container, 0); 9140 return; 9141 } 9142 9143 if (offset === undef) { 9144 domRange[start ? 'setStartBefore' : 'setEndAfter'](container); 9145 return; 9146 } 9147 9148 if (endPoint.position < 0) { 9149 sibling = endPoint.inside ? container.firstChild : container.nextSibling; 9150 9151 if (!sibling) { 9152 domRange[start ? 'setStartAfter' : 'setEndAfter'](container); 9153 return; 9154 } 9155 9156 if (!offset) { 9157 if (sibling.nodeType == 3) { 9158 domRange[start ? 'setStart' : 'setEnd'](sibling, 0); 9159 } else { 9160 domRange[start ? 'setStartBefore' : 'setEndBefore'](sibling); 9161 } 9162 9163 return; 9164 } 9165 9166 // Find the text node and offset 9167 while (sibling) { 9168 nodeValue = sibling.nodeValue; 9169 textNodeOffset += nodeValue.length; 9170 9171 // We are at or passed the position we where looking for 9172 if (textNodeOffset >= offset) { 9173 container = sibling; 9174 textNodeOffset -= offset; 9175 textNodeOffset = nodeValue.length - textNodeOffset; 9176 break; 9177 } 9178 9179 sibling = sibling.nextSibling; 9180 } 9181 } else { 9182 // Find the text node and offset 9183 sibling = container.previousSibling; 9184 9185 if (!sibling) { 9186 return domRange[start ? 'setStartBefore' : 'setEndBefore'](container); 9187 } 9188 9189 // If there isn't any text to loop then use the first position 9190 if (!offset) { 9191 if (container.nodeType == 3) { 9192 domRange[start ? 'setStart' : 'setEnd'](sibling, container.nodeValue.length); 9193 } else { 9194 domRange[start ? 'setStartAfter' : 'setEndAfter'](sibling); 9195 } 9196 9197 return; 9198 } 9199 9200 while (sibling) { 9201 textNodeOffset += sibling.nodeValue.length; 9202 9203 // We are at or passed the position we where looking for 9204 if (textNodeOffset >= offset) { 9205 container = sibling; 9206 textNodeOffset -= offset; 9207 break; 9208 } 9209 9210 sibling = sibling.previousSibling; 9211 } 9212 } 9213 9214 domRange[start ? 'setStart' : 'setEnd'](container, textNodeOffset); 9215 } 9216 9217 try { 9218 // Find start point 9219 findEndPoint(true); 9220 9221 // Find end point if needed 9222 if (!collapsed) { 9223 findEndPoint(); 9224 } 9225 } catch (ex) { 9226 // IE has a nasty bug where text nodes might throw "invalid argument" when you 9227 // access the nodeValue or other properties of text nodes. This seems to happend when 9228 // text nodes are split into two nodes by a delete/backspace call. So lets detect it and try to fix it. 9229 if (ex.number == -2147024809) { 9230 // Get the current selection 9231 bookmark = self.getBookmark(2); 9232 9233 // Get start element 9234 tmpRange = ieRange.duplicate(); 9235 tmpRange.collapse(true); 9236 element = tmpRange.parentElement(); 9237 9238 // Get end element 9239 if (!collapsed) { 9240 tmpRange = ieRange.duplicate(); 9241 tmpRange.collapse(false); 9242 element2 = tmpRange.parentElement(); 9243 element2.innerHTML = element2.innerHTML; 9244 } 9245 9246 // Remove the broken elements 9247 element.innerHTML = element.innerHTML; 9248 9249 // Restore the selection 9250 self.moveToBookmark(bookmark); 9251 9252 // Since the range has moved we need to re-get it 9253 ieRange = selection.getRng(); 9254 9255 // Find start point 9256 findEndPoint(true); 9257 9258 // Find end point if needed 9259 if (!collapsed) { 9260 findEndPoint(); 9261 } 9262 } else { 9263 throw ex; // Throw other errors 9264 } 9265 } 9266 9267 return domRange; 9268 } 9269 9270 this.getBookmark = function(type) { 9271 var rng = selection.getRng(), bookmark = {}; 9272 9273 function getIndexes(node) { 9274 var parent, root, children, i, indexes = []; 9275 9276 parent = node.parentNode; 9277 root = dom.getRoot().parentNode; 9278 9279 while (parent != root && parent.nodeType !== 9) { 9280 children = parent.children; 9281 9282 i = children.length; 9283 while (i--) { 9284 if (node === children[i]) { 9285 indexes.push(i); 9286 break; 9287 } 9288 } 9289 9290 node = parent; 9291 parent = parent.parentNode; 9292 } 9293 9294 return indexes; 9295 } 9296 9297 function getBookmarkEndPoint(start) { 9298 var position; 9299 9300 position = getPosition(rng, start); 9301 if (position) { 9302 return { 9303 position: position.position, 9304 offset: position.offset, 9305 indexes: getIndexes(position.node), 9306 inside: position.inside 9307 }; 9308 } 9309 } 9310 9311 // Non ubstructive bookmark 9312 if (type === 2) { 9313 // Handle text selection 9314 if (!rng.item) { 9315 bookmark.start = getBookmarkEndPoint(true); 9316 9317 if (!selection.isCollapsed()) { 9318 bookmark.end = getBookmarkEndPoint(); 9319 } 9320 } else { 9321 bookmark.start = {ctrl: true, indexes: getIndexes(rng.item(0))}; 9322 } 9323 } 9324 9325 return bookmark; 9326 }; 9327 9328 this.moveToBookmark = function(bookmark) { 9329 var rng, body = dom.doc.body; 9330 9331 function resolveIndexes(indexes) { 9332 var node, i, idx, children; 9333 9334 node = dom.getRoot(); 9335 for (i = indexes.length - 1; i >= 0; i--) { 9336 children = node.children; 9337 idx = indexes[i]; 9338 9339 if (idx <= children.length - 1) { 9340 node = children[idx]; 9341 } 9342 } 9343 9344 return node; 9345 } 9346 9347 function setBookmarkEndPoint(start) { 9348 var endPoint = bookmark[start ? 'start' : 'end'], moveLeft, moveRng, undef, offset; 9349 9350 if (endPoint) { 9351 moveLeft = endPoint.position > 0; 9352 9353 moveRng = body.createTextRange(); 9354 moveRng.moveToElementText(resolveIndexes(endPoint.indexes)); 9355 9356 offset = endPoint.offset; 9357 if (offset !== undef) { 9358 moveRng.collapse(endPoint.inside || moveLeft); 9359 moveRng.moveStart('character', moveLeft ? -offset : offset); 9360 } else { 9361 moveRng.collapse(start); 9362 } 9363 9364 rng.setEndPoint(start ? 'StartToStart' : 'EndToStart', moveRng); 9365 9366 if (start) { 9367 rng.collapse(true); 9368 } 9369 } 9370 } 9371 9372 if (bookmark.start) { 9373 if (bookmark.start.ctrl) { 9374 rng = body.createControlRange(); 9375 rng.addElement(resolveIndexes(bookmark.start.indexes)); 9376 rng.select(); 9377 } else { 9378 rng = body.createTextRange(); 9379 setBookmarkEndPoint(true); 9380 setBookmarkEndPoint(); 9381 rng.select(); 9382 } 9383 } 9384 }; 9385 9386 this.addRange = function(rng) { 9387 var ieRng, ctrlRng, startContainer, startOffset, endContainer, endOffset, sibling, 9388 doc = selection.dom.doc, body = doc.body, nativeRng, ctrlElm; 9389 9390 function setEndPoint(start) { 9391 var container, offset, marker, tmpRng, nodes; 9392 9393 marker = dom.create('a'); 9394 container = start ? startContainer : endContainer; 9395 offset = start ? startOffset : endOffset; 9396 tmpRng = ieRng.duplicate(); 9397 9398 if (container == doc || container == doc.documentElement) { 9399 container = body; 9400 offset = 0; 9401 } 9402 9403 if (container.nodeType == 3) { 9404 container.parentNode.insertBefore(marker, container); 9405 tmpRng.moveToElementText(marker); 9406 tmpRng.moveStart('character', offset); 9407 dom.remove(marker); 9408 ieRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', tmpRng); 9409 } else { 9410 nodes = container.childNodes; 9411 9412 if (nodes.length) { 9413 if (offset >= nodes.length) { 9414 dom.insertAfter(marker, nodes[nodes.length - 1]); 9415 } else { 9416 container.insertBefore(marker, nodes[offset]); 9417 } 9418 9419 tmpRng.moveToElementText(marker); 9420 } else if (container.canHaveHTML) { 9421 // Empty node selection for example <div>|</div> 9422 // Setting innerHTML with a span marker then remove that marker seems to keep empty block elements open 9423 container.innerHTML = '<span></span>'; 9424 marker = container.firstChild; 9425 tmpRng.moveToElementText(marker); 9426 tmpRng.collapse(FALSE); // Collapse false works better than true for some odd reason 9427 } 9428 9429 ieRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', tmpRng); 9430 dom.remove(marker); 9431 } 9432 } 9433 9434 // Setup some shorter versions 9435 startContainer = rng.startContainer; 9436 startOffset = rng.startOffset; 9437 endContainer = rng.endContainer; 9438 endOffset = rng.endOffset; 9439 ieRng = body.createTextRange(); 9440 9441 // If single element selection then try making a control selection out of it 9442 if (startContainer == endContainer && startContainer.nodeType == 1) { 9443 // Trick to place the caret inside an empty block element like <p></p> 9444 if (startOffset == endOffset && !startContainer.hasChildNodes()) { 9445 if (startContainer.canHaveHTML) { 9446 // Check if previous sibling is an empty block if it is then we need to render it 9447 // IE would otherwise move the caret into the sibling instead of the empty startContainer see: #5236 9448 // Example this: <p></p><p>|</p> would become this: <p>|</p><p></p> 9449 sibling = startContainer.previousSibling; 9450 if (sibling && !sibling.hasChildNodes() && dom.isBlock(sibling)) { 9451 sibling.innerHTML = ''; 9452 } else { 9453 sibling = null; 9454 } 9455 9456 startContainer.innerHTML = '<span></span><span></span>'; 9457 ieRng.moveToElementText(startContainer.lastChild); 9458 ieRng.select(); 9459 dom.doc.selection.clear(); 9460 startContainer.innerHTML = ''; 9461 9462 if (sibling) { 9463 sibling.innerHTML = ''; 9464 } 9465 return; 9466 } else { 9467 startOffset = dom.nodeIndex(startContainer); 9468 startContainer = startContainer.parentNode; 9469 } 9470 } 9471 9472 if (startOffset == endOffset - 1) { 9473 try { 9474 ctrlElm = startContainer.childNodes[startOffset]; 9475 ctrlRng = body.createControlRange(); 9476 ctrlRng.addElement(ctrlElm); 9477 ctrlRng.select(); 9478 9479 // Check if the range produced is on the correct element and is a control range 9480 // On IE 8 it will select the parent contentEditable container if you select an inner element see: #5398 9481 nativeRng = selection.getRng(); 9482 if (nativeRng.item && ctrlElm === nativeRng.item(0)) { 9483 return; 9484 } 9485 } catch (ex) { 9486 // Ignore 9487 } 9488 } 9489 } 9490 9491 // Set start/end point of selection 9492 setEndPoint(true); 9493 setEndPoint(); 9494 9495 // Select the new range and scroll it into view 9496 ieRng.select(); 9497 }; 9498 9499 // Expose range method 9500 this.getRangeAt = getRange; 9501 } 9502 9503 return Selection; 9504 }); 9505 9506 // Included from: js/tinymce/classes/util/VK.js 9507 9508 /** 9509 * VK.js 9510 * 9511 * Copyright, Moxiecode Systems AB 9512 * Released under LGPL License. 9513 * 9514 * License: http://www.tinymce.com/license 9515 * Contributing: http://www.tinymce.com/contributing 9516 */ 9517 9518 /** 9519 * This file exposes a set of the common KeyCodes for use. Please grow it as needed. 9520 */ 9521 define("tinymce/util/VK", [ 9522 "tinymce/Env" 9523 ], function(Env) { 9524 return { 9525 BACKSPACE: 8, 9526 DELETE: 46, 9527 DOWN: 40, 9528 ENTER: 13, 9529 LEFT: 37, 9530 RIGHT: 39, 9531 SPACEBAR: 32, 9532 TAB: 9, 9533 UP: 38, 9534 9535 modifierPressed: function(e) { 9536 return e.shiftKey || e.ctrlKey || e.altKey; 9537 }, 9538 9539 metaKeyPressed: function(e) { 9540 // Check if ctrl or meta key is pressed also check if alt is false for Polish users 9541 return (Env.mac ? e.metaKey : e.ctrlKey) && !e.altKey; 9542 } 9543 }; 9544 }); 9545 9546 // Included from: js/tinymce/classes/dom/ControlSelection.js 9547 9548 /** 9549 * ControlSelection.js 9550 * 9551 * Copyright, Moxiecode Systems AB 9552 * Released under LGPL License. 9553 * 9554 * License: http://www.tinymce.com/license 9555 * Contributing: http://www.tinymce.com/contributing 9556 */ 9557 9558 /** 9559 * This class handles control selection of elements. Controls are elements 9560 * that can be resized and needs to be selected as a whole. It adds custom resize handles 9561 * to all browser engines that support properly disabling the built in resize logic. 9562 * 9563 * @class tinymce.dom.ControlSelection 9564 */ 9565 define("tinymce/dom/ControlSelection", [ 9566 "tinymce/util/VK", 9567 "tinymce/util/Tools", 9568 "tinymce/Env" 9569 ], function(VK, Tools, Env) { 9570 return function(selection, editor) { 9571 var dom = editor.dom, each = Tools.each; 9572 var selectedElm, selectedElmGhost, resizeHandles, selectedHandle, lastMouseDownEvent; 9573 var startX, startY, selectedElmX, selectedElmY, startW, startH, ratio, resizeStarted; 9574 var width, height, editableDoc = editor.getDoc(), rootDocument = document, isIE = Env.ie && Env.ie < 11; 9575 9576 // Details about each resize handle how to scale etc 9577 resizeHandles = { 9578 // Name: x multiplier, y multiplier, delta size x, delta size y 9579 n: [0.5, 0, 0, -1], 9580 e: [1, 0.5, 1, 0], 9581 s: [0.5, 1, 0, 1], 9582 w: [0, 0.5, -1, 0], 9583 nw: [0, 0, -1, -1], 9584 ne: [1, 0, 1, -1], 9585 se: [1, 1, 1, 1], 9586 sw: [0, 1, -1, 1] 9587 }; 9588 9589 // Add CSS for resize handles, cloned element and selected 9590 var rootClass = '.mce-content-body'; 9591 editor.contentStyles.push( 9592 rootClass + ' div.mce-resizehandle {' + 9593 'position: absolute;' + 9594 'border: 1px solid black;' + 9595 'background: #FFF;' + 9596 'width: 5px;' + 9597 'height: 5px;' + 9598 'z-index: 10000' + 9599 '}' + 9600 rootClass + ' .mce-resizehandle:hover {' + 9601 'background: #000' + 9602 '}' + 9603 rootClass + ' img[data-mce-selected], hr[data-mce-selected] {' + 9604 'outline: 1px solid black;' + 9605 'resize: none' + // Have been talks about implementing this in browsers 9606 '}' + 9607 rootClass + ' .mce-clonedresizable {' + 9608 'position: absolute;' + 9609 (Env.gecko ? '' : 'outline: 1px dashed black;') + // Gecko produces trails while resizing 9610 'opacity: .5;' + 9611 'filter: alpha(opacity=50);' + 9612 'z-index: 10000' + 9613 '}' 9614 ); 9615 9616 function isResizable(elm) { 9617 var selector = editor.settings.object_resizing; 9618 9619 if (selector === false || Env.iOS) { 9620 return false; 9621 } 9622 9623 if (typeof selector != 'string') { 9624 selector = 'table,img,div'; 9625 } 9626 9627 if (elm.getAttribute('data-mce-resize') === 'false') { 9628 return false; 9629 } 9630 9631 return editor.dom.is(elm, selector); 9632 } 9633 9634 function resizeGhostElement(e) { 9635 var deltaX, deltaY; 9636 9637 // Calc new width/height 9638 deltaX = e.screenX - startX; 9639 deltaY = e.screenY - startY; 9640 9641 // Calc new size 9642 width = deltaX * selectedHandle[2] + startW; 9643 height = deltaY * selectedHandle[3] + startH; 9644 9645 // Never scale down lower than 5 pixels 9646 width = width < 5 ? 5 : width; 9647 height = height < 5 ? 5 : height; 9648 9649 // Constrain proportions when modifier key is pressed or if the nw, ne, sw, se corners are moved on an image 9650 if (VK.modifierPressed(e) || (selectedElm.nodeName == "IMG" && selectedHandle[2] * selectedHandle[3] !== 0)) { 9651 width = Math.round(height / ratio); 9652 height = Math.round(width * ratio); 9653 } 9654 9655 // Update ghost size 9656 dom.setStyles(selectedElmGhost, { 9657 width: width, 9658 height: height 9659 }); 9660 9661 // Update ghost X position if needed 9662 if (selectedHandle[2] < 0 && selectedElmGhost.clientWidth <= width) { 9663 dom.setStyle(selectedElmGhost, 'left', selectedElmX + (startW - width)); 9664 } 9665 9666 // Update ghost Y position if needed 9667 if (selectedHandle[3] < 0 && selectedElmGhost.clientHeight <= height) { 9668 dom.setStyle(selectedElmGhost, 'top', selectedElmY + (startH - height)); 9669 } 9670 9671 if (!resizeStarted) { 9672 editor.fire('ObjectResizeStart', {target: selectedElm, width: startW, height: startH}); 9673 resizeStarted = true; 9674 } 9675 } 9676 9677 function endGhostResize() { 9678 resizeStarted = false; 9679 9680 function setSizeProp(name, value) { 9681 if (value) { 9682 // Resize by using style or attribute 9683 if (selectedElm.style[name] || !editor.schema.isValid(selectedElm.nodeName.toLowerCase(), name)) { 9684 dom.setStyle(selectedElm, name, value); 9685 } else { 9686 dom.setAttrib(selectedElm, name, value); 9687 } 9688 } 9689 } 9690 9691 // Set width/height properties 9692 setSizeProp('width', width); 9693 setSizeProp('height', height); 9694 9695 dom.unbind(editableDoc, 'mousemove', resizeGhostElement); 9696 dom.unbind(editableDoc, 'mouseup', endGhostResize); 9697 9698 if (rootDocument != editableDoc) { 9699 dom.unbind(rootDocument, 'mousemove', resizeGhostElement); 9700 dom.unbind(rootDocument, 'mouseup', endGhostResize); 9701 } 9702 9703 // Remove ghost and update resize handle positions 9704 dom.remove(selectedElmGhost); 9705 9706 if (!isIE || selectedElm.nodeName == "TABLE") { 9707 showResizeRect(selectedElm); 9708 } 9709 9710 editor.fire('ObjectResized', {target: selectedElm, width: width, height: height}); 9711 editor.nodeChanged(); 9712 } 9713 9714 function showResizeRect(targetElm, mouseDownHandleName, mouseDownEvent) { 9715 var position, targetWidth, targetHeight, e, rect, offsetParent = editor.getBody(); 9716 9717 unbindResizeHandleEvents(); 9718 9719 // Get position and size of target 9720 position = dom.getPos(targetElm, offsetParent); 9721 selectedElmX = position.x; 9722 selectedElmY = position.y; 9723 rect = targetElm.getBoundingClientRect(); // Fix for Gecko offsetHeight for table with caption 9724 targetWidth = rect.width || (rect.right - rect.left); 9725 targetHeight = rect.height || (rect.bottom - rect.top); 9726 9727 // Reset width/height if user selects a new image/table 9728 if (selectedElm != targetElm) { 9729 detachResizeStartListener(); 9730 selectedElm = targetElm; 9731 width = height = 0; 9732 } 9733 9734 // Makes it possible to disable resizing 9735 e = editor.fire('ObjectSelected', {target: targetElm}); 9736 9737 if (isResizable(targetElm) && !e.isDefaultPrevented()) { 9738 each(resizeHandles, function(handle, name) { 9739 var handleElm, handlerContainerElm; 9740 9741 function startDrag(e) { 9742 startX = e.screenX; 9743 startY = e.screenY; 9744 startW = selectedElm.clientWidth; 9745 startH = selectedElm.clientHeight; 9746 ratio = startH / startW; 9747 selectedHandle = handle; 9748 9749 selectedElmGhost = selectedElm.cloneNode(true); 9750 dom.addClass(selectedElmGhost, 'mce-clonedresizable'); 9751 selectedElmGhost.contentEditable = false; // Hides IE move layer cursor 9752 selectedElmGhost.unSelectabe = true; 9753 dom.setStyles(selectedElmGhost, { 9754 left: selectedElmX, 9755 top: selectedElmY, 9756 margin: 0 9757 }); 9758 9759 selectedElmGhost.removeAttribute('data-mce-selected'); 9760 editor.getBody().appendChild(selectedElmGhost); 9761 9762 dom.bind(editableDoc, 'mousemove', resizeGhostElement); 9763 dom.bind(editableDoc, 'mouseup', endGhostResize); 9764 9765 if (rootDocument != editableDoc) { 9766 dom.bind(rootDocument, 'mousemove', resizeGhostElement); 9767 dom.bind(rootDocument, 'mouseup', endGhostResize); 9768 } 9769 } 9770 9771 if (mouseDownHandleName) { 9772 // Drag started by IE native resizestart 9773 if (name == mouseDownHandleName) { 9774 startDrag(mouseDownEvent); 9775 } 9776 9777 return; 9778 } 9779 9780 // Get existing or render resize handle 9781 handleElm = dom.get('mceResizeHandle' + name); 9782 if (!handleElm) { 9783 handlerContainerElm = editor.getBody(); 9784 9785 handleElm = dom.add(handlerContainerElm, 'div', { 9786 id: 'mceResizeHandle' + name, 9787 'data-mce-bogus': true, 9788 'class': 'mce-resizehandle', 9789 unselectable: true, 9790 style: 'cursor:' + name + '-resize; margin:0; padding:0' 9791 }); 9792 9793 // Hides IE move layer cursor 9794 // If we set it on Chrome we get this wounderful bug: #6725 9795 if (Env.ie) { 9796 handleElm.contentEditable = false; 9797 } 9798 } else { 9799 dom.show(handleElm); 9800 } 9801 9802 if (!handle.elm) { 9803 dom.bind(handleElm, 'mousedown', function(e) { 9804 e.stopImmediatePropagation(); 9805 e.preventDefault(); 9806 startDrag(e); 9807 }); 9808 9809 handle.elm = handleElm; 9810 } 9811 9812 /* 9813 var halfHandleW = handleElm.offsetWidth / 2; 9814 var halfHandleH = handleElm.offsetHeight / 2; 9815 9816 // Position element 9817 dom.setStyles(handleElm, { 9818 left: Math.floor((targetWidth * handle[0] + selectedElmX) - halfHandleW + (handle[2] * halfHandleW)), 9819 top: Math.floor((targetHeight * handle[1] + selectedElmY) - halfHandleH + (handle[3] * halfHandleH)) 9820 }); 9821 */ 9822 9823 // Position element 9824 dom.setStyles(handleElm, { 9825 left: (targetWidth * handle[0] + selectedElmX) - (handleElm.offsetWidth / 2), 9826 top: (targetHeight * handle[1] + selectedElmY) - (handleElm.offsetHeight / 2) 9827 }); 9828 }); 9829 } else { 9830 hideResizeRect(); 9831 } 9832 9833 selectedElm.setAttribute('data-mce-selected', '1'); 9834 } 9835 9836 function hideResizeRect() { 9837 var name, handleElm; 9838 9839 unbindResizeHandleEvents(); 9840 9841 if (selectedElm) { 9842 selectedElm.removeAttribute('data-mce-selected'); 9843 } 9844 9845 for (name in resizeHandles) { 9846 handleElm = dom.get('mceResizeHandle' + name); 9847 if (handleElm) { 9848 dom.unbind(handleElm); 9849 dom.remove(handleElm); 9850 } 9851 } 9852 } 9853 9854 function updateResizeRect(e) { 9855 var controlElm; 9856 9857 function isChildOrEqual(node, parent) { 9858 if (node) { 9859 do { 9860 if (node === parent) { 9861 return true; 9862 } 9863 } while ((node = node.parentNode)); 9864 } 9865 } 9866 9867 // Remove data-mce-selected from all elements since they might have been copied using Ctrl+c/v 9868 each(dom.select('img[data-mce-selected],hr[data-mce-selected]'), function(img) { 9869 img.removeAttribute('data-mce-selected'); 9870 }); 9871 9872 controlElm = e.type == 'mousedown' ? e.target : selection.getNode(); 9873 controlElm = dom.getParent(controlElm, isIE ? 'table' : 'table,img,hr'); 9874 9875 if (isChildOrEqual(controlElm, editor.getBody())) { 9876 disableGeckoResize(); 9877 9878 if (isChildOrEqual(selection.getStart(), controlElm) && isChildOrEqual(selection.getEnd(), controlElm)) { 9879 if (!isIE || (controlElm != selection.getStart() && selection.getStart().nodeName !== 'IMG')) { 9880 showResizeRect(controlElm); 9881 return; 9882 } 9883 } 9884 } 9885 9886 hideResizeRect(); 9887 } 9888 9889 function attachEvent(elm, name, func) { 9890 if (elm && elm.attachEvent) { 9891 elm.attachEvent('on' + name, func); 9892 } 9893 } 9894 9895 function detachEvent(elm, name, func) { 9896 if (elm && elm.detachEvent) { 9897 elm.detachEvent('on' + name, func); 9898 } 9899 } 9900 9901 function resizeNativeStart(e) { 9902 var target = e.srcElement, pos, name, corner, cornerX, cornerY, relativeX, relativeY; 9903 9904 pos = target.getBoundingClientRect(); 9905 relativeX = lastMouseDownEvent.clientX - pos.left; 9906 relativeY = lastMouseDownEvent.clientY - pos.top; 9907 9908 // Figure out what corner we are draging on 9909 for (name in resizeHandles) { 9910 corner = resizeHandles[name]; 9911 9912 cornerX = target.offsetWidth * corner[0]; 9913 cornerY = target.offsetHeight * corner[1]; 9914 9915 if (Math.abs(cornerX - relativeX) < 8 && Math.abs(cornerY - relativeY) < 8) { 9916 selectedHandle = corner; 9917 break; 9918 } 9919 } 9920 9921 // Remove native selection and let the magic begin 9922 resizeStarted = true; 9923 editor.getDoc().selection.empty(); 9924 showResizeRect(target, name, lastMouseDownEvent); 9925 } 9926 9927 function nativeControlSelect(e) { 9928 var target = e.srcElement; 9929 9930 if (target != selectedElm) { 9931 detachResizeStartListener(); 9932 9933 if (target.id.indexOf('mceResizeHandle') === 0) { 9934 e.returnValue = false; 9935 return; 9936 } 9937 9938 if (target.nodeName == 'IMG' || target.nodeName == 'TABLE') { 9939 hideResizeRect(); 9940 selectedElm = target; 9941 attachEvent(target, 'resizestart', resizeNativeStart); 9942 } 9943 } 9944 } 9945 9946 function detachResizeStartListener() { 9947 detachEvent(selectedElm, 'resizestart', resizeNativeStart); 9948 } 9949 9950 function unbindResizeHandleEvents() { 9951 for (var name in resizeHandles) { 9952 var handle = resizeHandles[name]; 9953 9954 if (handle.elm) { 9955 dom.unbind(handle.elm); 9956 delete handle.elm; 9957 } 9958 } 9959 } 9960 9961 function disableGeckoResize() { 9962 try { 9963 // Disable object resizing on Gecko 9964 editor.getDoc().execCommand('enableObjectResizing', false, false); 9965 } catch (ex) { 9966 // Ignore 9967 } 9968 } 9969 9970 function controlSelect(elm) { 9971 var ctrlRng; 9972 9973 if (!isIE) { 9974 return; 9975 } 9976 9977 ctrlRng = editableDoc.body.createControlRange(); 9978 9979 try { 9980 ctrlRng.addElement(elm); 9981 ctrlRng.select(); 9982 return true; 9983 } catch (ex) { 9984 // Ignore since the element can't be control selected for example a P tag 9985 } 9986 } 9987 9988 editor.on('init', function() { 9989 if (isIE) { 9990 // Hide the resize rect on resize and reselect the image 9991 editor.on('ObjectResized', function(e) { 9992 if (e.target.nodeName != 'TABLE') { 9993 hideResizeRect(); 9994 controlSelect(e.target); 9995 } 9996 }); 9997 9998 attachEvent(editor.getBody(), 'controlselect', nativeControlSelect); 9999 10000 editor.on('mousedown', function(e) { 10001 lastMouseDownEvent = e; 10002 }); 10003 } else { 10004 disableGeckoResize(); 10005 10006 if (Env.ie >= 11) { 10007 // TODO: Drag/drop doesn't work 10008 editor.on('mouseup', function(e) { 10009 var nodeName = e.target.nodeName; 10010 10011 if (/^(TABLE|IMG|HR)$/.test(nodeName)) { 10012 editor.selection.select(e.target, nodeName == 'TABLE'); 10013 editor.nodeChanged(); 10014 } 10015 }); 10016 10017 editor.dom.bind(editor.getBody(), 'mscontrolselect', function(e) { 10018 if (/^(TABLE|IMG|HR)$/.test(e.target.nodeName)) { 10019 e.preventDefault(); 10020 10021 // This moves the selection from being a control selection to a text like selection like in WebKit #6753 10022 // TODO: Fix this the day IE works like other browsers without this nasty native ugly control selections. 10023 if (e.target.tagName == 'IMG') { 10024 window.setTimeout(function() { 10025 editor.selection.select(e.target); 10026 }, 0); 10027 } 10028 } 10029 }); 10030 } 10031 } 10032 10033 editor.on('nodechange mousedown mouseup ResizeEditor', updateResizeRect); 10034 10035 // Update resize rect while typing in a table 10036 editor.on('keydown keyup', function(e) { 10037 if (selectedElm && selectedElm.nodeName == "TABLE") { 10038 updateResizeRect(e); 10039 } 10040 }); 10041 10042 editor.on('hide', hideResizeRect); 10043 10044 // Hide rect on focusout since it would float on top of windows otherwise 10045 //editor.on('focusout', hideResizeRect); 10046 }); 10047 10048 editor.on('remove', unbindResizeHandleEvents); 10049 10050 function destroy() { 10051 selectedElm = selectedElmGhost = null; 10052 10053 if (isIE) { 10054 detachResizeStartListener(); 10055 detachEvent(editor.getBody(), 'controlselect', nativeControlSelect); 10056 } 10057 } 10058 10059 return { 10060 isResizable: isResizable, 10061 showResizeRect: showResizeRect, 10062 hideResizeRect: hideResizeRect, 10063 updateResizeRect: updateResizeRect, 10064 controlSelect: controlSelect, 10065 destroy: destroy 10066 }; 10067 }; 10068 }); 10069 10070 // Included from: js/tinymce/classes/dom/RangeUtils.js 10071 10072 /** 10073 * Range.js 10074 * 10075 * Copyright, Moxiecode Systems AB 10076 * Released under LGPL License. 10077 * 10078 * License: http://www.tinymce.com/license 10079 * Contributing: http://www.tinymce.com/contributing 10080 */ 10081 10082 /** 10083 * RangeUtils 10084 * 10085 * @class tinymce.dom.RangeUtils 10086 * @private 10087 */ 10088 define("tinymce/dom/RangeUtils", [ 10089 "tinymce/util/Tools", 10090 "tinymce/dom/TreeWalker" 10091 ], function(Tools, TreeWalker) { 10092 var each = Tools.each; 10093 10094 function RangeUtils(dom) { 10095 /** 10096 * Walks the specified range like object and executes the callback for each sibling collection it finds. 10097 * 10098 * @method walk 10099 * @param {Object} rng Range like object. 10100 * @param {function} callback Callback function to execute for each sibling collection. 10101 */ 10102 this.walk = function(rng, callback) { 10103 var startContainer = rng.startContainer, 10104 startOffset = rng.startOffset, 10105 endContainer = rng.endContainer, 10106 endOffset = rng.endOffset, 10107 ancestor, startPoint, 10108 endPoint, node, parent, siblings, nodes; 10109 10110 // Handle table cell selection the table plugin enables 10111 // you to fake select table cells and perform formatting actions on them 10112 nodes = dom.select('td.mce-item-selected,th.mce-item-selected'); 10113 if (nodes.length > 0) { 10114 each(nodes, function(node) { 10115 callback([node]); 10116 }); 10117 10118 return; 10119 } 10120 10121 /** 10122 * Excludes start/end text node if they are out side the range 10123 * 10124 * @private 10125 * @param {Array} nodes Nodes to exclude items from. 10126 * @return {Array} Array with nodes excluding the start/end container if needed. 10127 */ 10128 function exclude(nodes) { 10129 var node; 10130 10131 // First node is excluded 10132 node = nodes[0]; 10133 if (node.nodeType === 3 && node === startContainer && startOffset >= node.nodeValue.length) { 10134 nodes.splice(0, 1); 10135 } 10136 10137 // Last node is excluded 10138 node = nodes[nodes.length - 1]; 10139 if (endOffset === 0 && nodes.length > 0 && node === endContainer && node.nodeType === 3) { 10140 nodes.splice(nodes.length - 1, 1); 10141 } 10142 10143 return nodes; 10144 } 10145 10146 /** 10147 * Collects siblings 10148 * 10149 * @private 10150 * @param {Node} node Node to collect siblings from. 10151 * @param {String} name Name of the sibling to check for. 10152 * @return {Array} Array of collected siblings. 10153 */ 10154 function collectSiblings(node, name, end_node) { 10155 var siblings = []; 10156 10157 for (; node && node != end_node; node = node[name]) { 10158 siblings.push(node); 10159 } 10160 10161 return siblings; 10162 } 10163 10164 /** 10165 * Find an end point this is the node just before the common ancestor root. 10166 * 10167 * @private 10168 * @param {Node} node Node to start at. 10169 * @param {Node} root Root/ancestor element to stop just before. 10170 * @return {Node} Node just before the root element. 10171 */ 10172 function findEndPoint(node, root) { 10173 do { 10174 if (node.parentNode == root) { 10175 return node; 10176 } 10177 10178 node = node.parentNode; 10179 } while(node); 10180 } 10181 10182 function walkBoundary(start_node, end_node, next) { 10183 var siblingName = next ? 'nextSibling' : 'previousSibling'; 10184 10185 for (node = start_node, parent = node.parentNode; node && node != end_node; node = parent) { 10186 parent = node.parentNode; 10187 siblings = collectSiblings(node == start_node ? node : node[siblingName], siblingName); 10188 10189 if (siblings.length) { 10190 if (!next) { 10191 siblings.reverse(); 10192 } 10193 10194 callback(exclude(siblings)); 10195 } 10196 } 10197 } 10198 10199 // If index based start position then resolve it 10200 if (startContainer.nodeType == 1 && startContainer.hasChildNodes()) { 10201 startContainer = startContainer.childNodes[startOffset]; 10202 } 10203 10204 // If index based end position then resolve it 10205 if (endContainer.nodeType == 1 && endContainer.hasChildNodes()) { 10206 endContainer = endContainer.childNodes[Math.min(endOffset - 1, endContainer.childNodes.length - 1)]; 10207 } 10208 10209 // Same container 10210 if (startContainer == endContainer) { 10211 return callback(exclude([startContainer])); 10212 } 10213 10214 // Find common ancestor and end points 10215 ancestor = dom.findCommonAncestor(startContainer, endContainer); 10216 10217 // Process left side 10218 for (node = startContainer; node; node = node.parentNode) { 10219 if (node === endContainer) { 10220 return walkBoundary(startContainer, ancestor, true); 10221 } 10222 10223 if (node === ancestor) { 10224 break; 10225 } 10226 } 10227 10228 // Process right side 10229 for (node = endContainer; node; node = node.parentNode) { 10230 if (node === startContainer) { 10231 return walkBoundary(endContainer, ancestor); 10232 } 10233 10234 if (node === ancestor) { 10235 break; 10236 } 10237 } 10238 10239 // Find start/end point 10240 startPoint = findEndPoint(startContainer, ancestor) || startContainer; 10241 endPoint = findEndPoint(endContainer, ancestor) || endContainer; 10242 10243 // Walk left leaf 10244 walkBoundary(startContainer, startPoint, true); 10245 10246 // Walk the middle from start to end point 10247 siblings = collectSiblings( 10248 startPoint == startContainer ? startPoint : startPoint.nextSibling, 10249 'nextSibling', 10250 endPoint == endContainer ? endPoint.nextSibling : endPoint 10251 ); 10252 10253 if (siblings.length) { 10254 callback(exclude(siblings)); 10255 } 10256 10257 // Walk right leaf 10258 walkBoundary(endContainer, endPoint); 10259 }; 10260 10261 /** 10262 * Splits the specified range at it's start/end points. 10263 * 10264 * @private 10265 * @param {Range/RangeObject} rng Range to split. 10266 * @return {Object} Range position object. 10267 */ 10268 this.split = function(rng) { 10269 var startContainer = rng.startContainer, 10270 startOffset = rng.startOffset, 10271 endContainer = rng.endContainer, 10272 endOffset = rng.endOffset; 10273 10274 function splitText(node, offset) { 10275 return node.splitText(offset); 10276 } 10277 10278 // Handle single text node 10279 if (startContainer == endContainer && startContainer.nodeType == 3) { 10280 if (startOffset > 0 && startOffset < startContainer.nodeValue.length) { 10281 endContainer = splitText(startContainer, startOffset); 10282 startContainer = endContainer.previousSibling; 10283 10284 if (endOffset > startOffset) { 10285 endOffset = endOffset - startOffset; 10286 startContainer = endContainer = splitText(endContainer, endOffset).previousSibling; 10287 endOffset = endContainer.nodeValue.length; 10288 startOffset = 0; 10289 } else { 10290 endOffset = 0; 10291 } 10292 } 10293 } else { 10294 // Split startContainer text node if needed 10295 if (startContainer.nodeType == 3 && startOffset > 0 && startOffset < startContainer.nodeValue.length) { 10296 startContainer = splitText(startContainer, startOffset); 10297 startOffset = 0; 10298 } 10299 10300 // Split endContainer text node if needed 10301 if (endContainer.nodeType == 3 && endOffset > 0 && endOffset < endContainer.nodeValue.length) { 10302 endContainer = splitText(endContainer, endOffset).previousSibling; 10303 endOffset = endContainer.nodeValue.length; 10304 } 10305 } 10306 10307 return { 10308 startContainer: startContainer, 10309 startOffset: startOffset, 10310 endContainer: endContainer, 10311 endOffset: endOffset 10312 }; 10313 }; 10314 10315 /** 10316 * Normalizes the specified range by finding the closest best suitable caret location. 10317 * 10318 * @private 10319 * @param {Range} rng Range to normalize. 10320 * @return {Boolean} True/false if the specified range was normalized or not. 10321 */ 10322 this.normalize = function(rng) { 10323 var normalized, collapsed; 10324 10325 function normalizeEndPoint(start) { 10326 var container, offset, walker, body = dom.getRoot(), node, nonEmptyElementsMap; 10327 var directionLeft, isAfterNode; 10328 10329 function hasBrBeforeAfter(node, left) { 10330 var walker = new TreeWalker(node, dom.getParent(node.parentNode, dom.isBlock) || body); 10331 10332 while ((node = walker[left ? 'prev' : 'next']())) { 10333 if (node.nodeName === "BR") { 10334 return true; 10335 } 10336 } 10337 } 10338 10339 function isPrevNode(node, name) { 10340 return node.previousSibling && node.previousSibling.nodeName == name; 10341 } 10342 10343 // Walks the dom left/right to find a suitable text node to move the endpoint into 10344 // It will only walk within the current parent block or body and will stop if it hits a block or a BR/IMG 10345 function findTextNodeRelative(left, startNode) { 10346 var walker, lastInlineElement, parentBlockContainer; 10347 10348 startNode = startNode || container; 10349 parentBlockContainer = dom.getParent(startNode.parentNode, dom.isBlock) || body; 10350 10351 // Lean left before the BR element if it's the only BR within a block element. Gecko bug: #6680 10352 // This: <p><br>|</p> becomes <p>|<br></p> 10353 if (left && startNode.nodeName == 'BR' && isAfterNode && dom.isEmpty(parentBlockContainer)) { 10354 container = startNode.parentNode; 10355 offset = dom.nodeIndex(startNode); 10356 normalized = true; 10357 return; 10358 } 10359 10360 // Walk left until we hit a text node we can move to or a block/br/img 10361 walker = new TreeWalker(startNode, parentBlockContainer); 10362 while ((node = walker[left ? 'prev' : 'next']())) { 10363 // Break if we hit a non content editable node 10364 if (dom.getContentEditableParent(node) === "false") { 10365 return; 10366 } 10367 10368 // Found text node that has a length 10369 if (node.nodeType === 3 && node.nodeValue.length > 0) { 10370 container = node; 10371 offset = left ? node.nodeValue.length : 0; 10372 normalized = true; 10373 return; 10374 } 10375 10376 // Break if we find a block or a BR/IMG/INPUT etc 10377 if (dom.isBlock(node) || nonEmptyElementsMap[node.nodeName.toLowerCase()]) { 10378 return; 10379 } 10380 10381 lastInlineElement = node; 10382 } 10383 10384 // Only fetch the last inline element when in caret mode for now 10385 if (collapsed && lastInlineElement) { 10386 container = lastInlineElement; 10387 normalized = true; 10388 offset = 0; 10389 } 10390 } 10391 10392 container = rng[(start ? 'start' : 'end') + 'Container']; 10393 offset = rng[(start ? 'start' : 'end') + 'Offset']; 10394 isAfterNode = container.nodeType == 1 && offset === container.childNodes.length; 10395 nonEmptyElementsMap = dom.schema.getNonEmptyElements(); 10396 directionLeft = start; 10397 10398 if (container.nodeType == 1 && offset > container.childNodes.length - 1) { 10399 directionLeft = false; 10400 } 10401 10402 // If the container is a document move it to the body element 10403 if (container.nodeType === 9) { 10404 container = dom.getRoot(); 10405 offset = 0; 10406 } 10407 10408 // If the container is body try move it into the closest text node or position 10409 if (container === body) { 10410 // If start is before/after a image, table etc 10411 if (directionLeft) { 10412 node = container.childNodes[offset > 0 ? offset - 1 : 0]; 10413 if (node) { 10414 if (nonEmptyElementsMap[node.nodeName] || node.nodeName == "TABLE") { 10415 return; 10416 } 10417 } 10418 } 10419 10420 // Resolve the index 10421 if (container.hasChildNodes()) { 10422 offset = Math.min(!directionLeft && offset > 0 ? offset - 1 : offset, container.childNodes.length - 1); 10423 container = container.childNodes[offset]; 10424 offset = 0; 10425 10426 // Don't walk into elements that doesn't have any child nodes like a IMG 10427 if (container.hasChildNodes() && !/TABLE/.test(container.nodeName)) { 10428 // Walk the DOM to find a text node to place the caret at or a BR 10429 node = container; 10430 walker = new TreeWalker(container, body); 10431 10432 do { 10433 // Found a text node use that position 10434 if (node.nodeType === 3 && node.nodeValue.length > 0) { 10435 offset = directionLeft ? 0 : node.nodeValue.length; 10436 container = node; 10437 normalized = true; 10438 break; 10439 } 10440 10441 // Found a BR/IMG element that we can place the caret before 10442 if (nonEmptyElementsMap[node.nodeName.toLowerCase()]) { 10443 offset = dom.nodeIndex(node); 10444 container = node.parentNode; 10445 10446 // Put caret after image when moving the end point 10447 if (node.nodeName == "IMG" && !directionLeft) { 10448 offset++; 10449 } 10450 10451 normalized = true; 10452 break; 10453 } 10454 } while ((node = (directionLeft ? walker.next() : walker.prev()))); 10455 } 10456 } 10457 } 10458 10459 // Lean the caret to the left if possible 10460 if (collapsed) { 10461 // So this: <b>x</b><i>|x</i> 10462 // Becomes: <b>x|</b><i>x</i> 10463 // Seems that only gecko has issues with this 10464 if (container.nodeType === 3 && offset === 0) { 10465 findTextNodeRelative(true); 10466 } 10467 10468 // Lean left into empty inline elements when the caret is before a BR 10469 // So this: <i><b></b><i>|<br></i> 10470 // Becomes: <i><b>|</b><i><br></i> 10471 // Seems that only gecko has issues with this. 10472 // Special edge case for <p><a>x</a>|<br></p> since we don't want <p><a>x|</a><br></p> 10473 if (container.nodeType === 1) { 10474 node = container.childNodes[offset]; 10475 10476 // Offset is after the containers last child 10477 // then use the previous child for normalization 10478 if (!node) { 10479 node = container.childNodes[offset - 1]; 10480 } 10481 10482 if (node && node.nodeName === 'BR' && !isPrevNode(node, 'A') && 10483 !hasBrBeforeAfter(node) && !hasBrBeforeAfter(node, true)) { 10484 findTextNodeRelative(true, node); 10485 } 10486 } 10487 } 10488 10489 // Lean the start of the selection right if possible 10490 // So this: x[<b>x]</b> 10491 // Becomes: x<b>[x]</b> 10492 if (directionLeft && !collapsed && container.nodeType === 3 && offset === container.nodeValue.length) { 10493 findTextNodeRelative(false); 10494 } 10495 10496 // Set endpoint if it was normalized 10497 if (normalized) { 10498 rng['set' + (start ? 'Start' : 'End')](container, offset); 10499 } 10500 } 10501 10502 collapsed = rng.collapsed; 10503 10504 normalizeEndPoint(true); 10505 10506 if (!collapsed) { 10507 normalizeEndPoint(); 10508 } 10509 10510 // If it was collapsed then make sure it still is 10511 if (normalized && collapsed) { 10512 rng.collapse(true); 10513 } 10514 10515 return normalized; 10516 }; 10517 } 10518 10519 /** 10520 * Compares two ranges and checks if they are equal. 10521 * 10522 * @static 10523 * @method compareRanges 10524 * @param {DOMRange} rng1 First range to compare. 10525 * @param {DOMRange} rng2 First range to compare. 10526 * @return {Boolean} true/false if the ranges are equal. 10527 */ 10528 RangeUtils.compareRanges = function(rng1, rng2) { 10529 if (rng1 && rng2) { 10530 // Compare native IE ranges 10531 if (rng1.item || rng1.duplicate) { 10532 // Both are control ranges and the selected element matches 10533 if (rng1.item && rng2.item && rng1.item(0) === rng2.item(0)) { 10534 return true; 10535 } 10536 10537 // Both are text ranges and the range matches 10538 if (rng1.isEqual && rng2.isEqual && rng2.isEqual(rng1)) { 10539 return true; 10540 } 10541 } else { 10542 // Compare w3c ranges 10543 return rng1.startContainer == rng2.startContainer && rng1.startOffset == rng2.startOffset; 10544 } 10545 } 10546 10547 return false; 10548 }; 10549 10550 return RangeUtils; 10551 }); 10552 10553 // Included from: js/tinymce/classes/dom/Selection.js 10554 10555 /** 10556 * Selection.js 10557 * 10558 * Copyright, Moxiecode Systems AB 10559 * Released under LGPL License. 10560 * 10561 * License: http://www.tinymce.com/license 10562 * Contributing: http://www.tinymce.com/contributing 10563 */ 10564 10565 /** 10566 * This class handles text and control selection it's an crossbrowser utility class. 10567 * Consult the TinyMCE Wiki API for more details and examples on how to use this class. 10568 * 10569 * @class tinymce.dom.Selection 10570 * @example 10571 * // Getting the currently selected node for the active editor 10572 * alert(tinymce.activeEditor.selection.getNode().nodeName); 10573 */ 10574 define("tinymce/dom/Selection", [ 10575 "tinymce/dom/TreeWalker", 10576 "tinymce/dom/TridentSelection", 10577 "tinymce/dom/ControlSelection", 10578 "tinymce/dom/RangeUtils", 10579 "tinymce/Env", 10580 "tinymce/util/Tools" 10581 ], function(TreeWalker, TridentSelection, ControlSelection, RangeUtils, Env, Tools) { 10582 var each = Tools.each, grep = Tools.grep, trim = Tools.trim; 10583 var isIE = Env.ie, isOpera = Env.opera; 10584 10585 /** 10586 * Constructs a new selection instance. 10587 * 10588 * @constructor 10589 * @method Selection 10590 * @param {tinymce.dom.DOMUtils} dom DOMUtils object reference. 10591 * @param {Window} win Window to bind the selection object to. 10592 * @param {tinymce.dom.Serializer} serializer DOM serialization class to use for getContent. 10593 */ 10594 function Selection(dom, win, serializer, editor) { 10595 var self = this; 10596 10597 self.dom = dom; 10598 self.win = win; 10599 self.serializer = serializer; 10600 self.editor = editor; 10601 10602 self.controlSelection = new ControlSelection(self, editor); 10603 10604 // No W3C Range support 10605 if (!self.win.getSelection) { 10606 self.tridentSel = new TridentSelection(self); 10607 } 10608 } 10609 10610 Selection.prototype = { 10611 /** 10612 * Move the selection cursor range to the specified node and offset. 10613 * If there is no node specified it will move it to the first suitable location within the body. 10614 * 10615 * @method setCursorLocation 10616 * @param {Node} node Optional node to put the cursor in. 10617 * @param {Number} offset Optional offset from the start of the node to put the cursor at. 10618 */ 10619 setCursorLocation: function(node, offset) { 10620 var self = this, rng = self.dom.createRng(); 10621 10622 if (!node) { 10623 self._moveEndPoint(rng, self.editor.getBody(), true); 10624 self.setRng(rng); 10625 } else { 10626 rng.setStart(node, offset); 10627 rng.setEnd(node, offset); 10628 self.setRng(rng); 10629 self.collapse(false); 10630 } 10631 }, 10632 10633 /** 10634 * Returns the selected contents using the DOM serializer passed in to this class. 10635 * 10636 * @method getContent 10637 * @param {Object} s Optional settings class with for example output format text or html. 10638 * @return {String} Selected contents in for example HTML format. 10639 * @example 10640 * // Alerts the currently selected contents 10641 * alert(tinymce.activeEditor.selection.getContent()); 10642 * 10643 * // Alerts the currently selected contents as plain text 10644 * alert(tinymce.activeEditor.selection.getContent({format: 'text'})); 10645 */ 10646 getContent: function(args) { 10647 var self = this, rng = self.getRng(), tmpElm = self.dom.create("body"); 10648 var se = self.getSel(), whiteSpaceBefore, whiteSpaceAfter, fragment; 10649 10650 args = args || {}; 10651 whiteSpaceBefore = whiteSpaceAfter = ''; 10652 args.get = true; 10653 args.format = args.format || 'html'; 10654 args.selection = true; 10655 self.editor.fire('BeforeGetContent', args); 10656 10657 if (args.format == 'text') { 10658 return self.isCollapsed() ? '' : (rng.text || (se.toString ? se.toString() : '')); 10659 } 10660 10661 if (rng.cloneContents) { 10662 fragment = rng.cloneContents(); 10663 10664 if (fragment) { 10665 tmpElm.appendChild(fragment); 10666 } 10667 } else if (rng.item !== undefined || rng.htmlText !== undefined) { 10668 // IE will produce invalid markup if elements are present that 10669 // it doesn't understand like custom elements or HTML5 elements. 10670 // Adding a BR in front of the contents and then remoiving it seems to fix it though. 10671 tmpElm.innerHTML = '<br>' + (rng.item ? rng.item(0).outerHTML : rng.htmlText); 10672 tmpElm.removeChild(tmpElm.firstChild); 10673 } else { 10674 tmpElm.innerHTML = rng.toString(); 10675 } 10676 10677 // Keep whitespace before and after 10678 if (/^\s/.test(tmpElm.innerHTML)) { 10679 whiteSpaceBefore = ' '; 10680 } 10681 10682 if (/\s+$/.test(tmpElm.innerHTML)) { 10683 whiteSpaceAfter = ' '; 10684 } 10685 10686 args.getInner = true; 10687 10688 args.content = self.isCollapsed() ? '' : whiteSpaceBefore + self.serializer.serialize(tmpElm, args) + whiteSpaceAfter; 10689 self.editor.fire('GetContent', args); 10690 10691 return args.content; 10692 }, 10693 10694 /** 10695 * Sets the current selection to the specified content. If any contents is selected it will be replaced 10696 * with the contents passed in to this function. If there is no selection the contents will be inserted 10697 * where the caret is placed in the editor/page. 10698 * 10699 * @method setContent 10700 * @param {String} content HTML contents to set could also be other formats depending on settings. 10701 * @param {Object} args Optional settings object with for example data format. 10702 * @example 10703 * // Inserts some HTML contents at the current selection 10704 * tinymce.activeEditor.selection.setContent('<strong>Some contents</strong>'); 10705 */ 10706 setContent: function(content, args) { 10707 var self = this, rng = self.getRng(), caretNode, doc = self.win.document, frag, temp; 10708 10709 args = args || {format: 'html'}; 10710 args.set = true; 10711 args.selection = true; 10712 content = args.content = content; 10713 10714 // Dispatch before set content event 10715 if (!args.no_events) { 10716 self.editor.fire('BeforeSetContent', args); 10717 } 10718 10719 content = args.content; 10720 10721 if (rng.insertNode) { 10722 // Make caret marker since insertNode places the caret in the beginning of text after insert 10723 content += '<span id="__caret">_</span>'; 10724 10725 // Delete and insert new node 10726 if (rng.startContainer == doc && rng.endContainer == doc) { 10727 // WebKit will fail if the body is empty since the range is then invalid and it can't insert contents 10728 doc.body.innerHTML = content; 10729 } else { 10730 rng.deleteContents(); 10731 10732 if (doc.body.childNodes.length === 0) { 10733 doc.body.innerHTML = content; 10734 } else { 10735 // createContextualFragment doesn't exists in IE 9 DOMRanges 10736 if (rng.createContextualFragment) { 10737 rng.insertNode(rng.createContextualFragment(content)); 10738 } else { 10739 // Fake createContextualFragment call in IE 9 10740 frag = doc.createDocumentFragment(); 10741 temp = doc.createElement('div'); 10742 10743 frag.appendChild(temp); 10744 temp.outerHTML = content; 10745 10746 rng.insertNode(frag); 10747 } 10748 } 10749 } 10750 10751 // Move to caret marker 10752 caretNode = self.dom.get('__caret'); 10753 10754 // Make sure we wrap it compleatly, Opera fails with a simple select call 10755 rng = doc.createRange(); 10756 rng.setStartBefore(caretNode); 10757 rng.setEndBefore(caretNode); 10758 self.setRng(rng); 10759 10760 // Remove the caret position 10761 self.dom.remove('__caret'); 10762 10763 try { 10764 self.setRng(rng); 10765 } catch (ex) { 10766 // Might fail on Opera for some odd reason 10767 } 10768 } else { 10769 if (rng.item) { 10770 // Delete content and get caret text selection 10771 doc.execCommand('Delete', false, null); 10772 rng = self.getRng(); 10773 } 10774 10775 // Explorer removes spaces from the beginning of pasted contents 10776 if (/^\s+/.test(content)) { 10777 rng.pasteHTML('<span id="__mce_tmp">_</span>' + content); 10778 self.dom.remove('__mce_tmp'); 10779 } else { 10780 rng.pasteHTML(content); 10781 } 10782 } 10783 10784 // Dispatch set content event 10785 if (!args.no_events) { 10786 self.editor.fire('SetContent', args); 10787 } 10788 }, 10789 10790 /** 10791 * Returns the start element of a selection range. If the start is in a text 10792 * node the parent element will be returned. 10793 * 10794 * @method getStart 10795 * @return {Element} Start element of selection range. 10796 */ 10797 getStart: function() { 10798 var self = this, rng = self.getRng(), startElement, parentElement, checkRng, node; 10799 10800 if (rng.duplicate || rng.item) { 10801 // Control selection, return first item 10802 if (rng.item) { 10803 return rng.item(0); 10804 } 10805 10806 // Get start element 10807 checkRng = rng.duplicate(); 10808 checkRng.collapse(1); 10809 startElement = checkRng.parentElement(); 10810 if (startElement.ownerDocument !== self.dom.doc) { 10811 startElement = self.dom.getRoot(); 10812 } 10813 10814 // Check if range parent is inside the start element, then return the inner parent element 10815 // This will fix issues when a single element is selected, IE would otherwise return the wrong start element 10816 parentElement = node = rng.parentElement(); 10817 while ((node = node.parentNode)) { 10818 if (node == startElement) { 10819 startElement = parentElement; 10820 break; 10821 } 10822 } 10823 10824 return startElement; 10825 } else { 10826 startElement = rng.startContainer; 10827 10828 if (startElement.nodeType == 1 && startElement.hasChildNodes()) { 10829 startElement = startElement.childNodes[Math.min(startElement.childNodes.length - 1, rng.startOffset)]; 10830 } 10831 10832 if (startElement && startElement.nodeType == 3) { 10833 return startElement.parentNode; 10834 } 10835 10836 return startElement; 10837 } 10838 }, 10839 10840 /** 10841 * Returns the end element of a selection range. If the end is in a text 10842 * node the parent element will be returned. 10843 * 10844 * @method getEnd 10845 * @return {Element} End element of selection range. 10846 */ 10847 getEnd: function() { 10848 var self = this, rng = self.getRng(), endElement, endOffset; 10849 10850 if (rng.duplicate || rng.item) { 10851 if (rng.item) { 10852 return rng.item(0); 10853 } 10854 10855 rng = rng.duplicate(); 10856 rng.collapse(0); 10857 endElement = rng.parentElement(); 10858 if (endElement.ownerDocument !== self.dom.doc) { 10859 endElement = self.dom.getRoot(); 10860 } 10861 10862 if (endElement && endElement.nodeName == 'BODY') { 10863 return endElement.lastChild || endElement; 10864 } 10865 10866 return endElement; 10867 } else { 10868 endElement = rng.endContainer; 10869 endOffset = rng.endOffset; 10870 10871 if (endElement.nodeType == 1 && endElement.hasChildNodes()) { 10872 endElement = endElement.childNodes[endOffset > 0 ? endOffset - 1 : endOffset]; 10873 } 10874 10875 if (endElement && endElement.nodeType == 3) { 10876 return endElement.parentNode; 10877 } 10878 10879 return endElement; 10880 } 10881 }, 10882 10883 /** 10884 * Returns a bookmark location for the current selection. This bookmark object 10885 * can then be used to restore the selection after some content modification to the document. 10886 * 10887 * @method getBookmark 10888 * @param {Number} type Optional state if the bookmark should be simple or not. Default is complex. 10889 * @param {Boolean} normalized Optional state that enables you to get a position that it would be after normalization. 10890 * @return {Object} Bookmark object, use moveToBookmark with this object to restore the selection. 10891 * @example 10892 * // Stores a bookmark of the current selection 10893 * var bm = tinymce.activeEditor.selection.getBookmark(); 10894 * 10895 * tinymce.activeEditor.setContent(tinymce.activeEditor.getContent() + 'Some new content'); 10896 * 10897 * // Restore the selection bookmark 10898 * tinymce.activeEditor.selection.moveToBookmark(bm); 10899 */ 10900 getBookmark: function(type, normalized) { 10901 var self = this, dom = self.dom, rng, rng2, id, collapsed, name, element, chr = '', styles; 10902 10903 function findIndex(name, element) { 10904 var index = 0; 10905 10906 each(dom.select(name), function(node, i) { 10907 if (node == element) { 10908 index = i; 10909 } 10910 }); 10911 10912 return index; 10913 } 10914 10915 function normalizeTableCellSelection(rng) { 10916 function moveEndPoint(start) { 10917 var container, offset, childNodes, prefix = start ? 'start' : 'end'; 10918 10919 container = rng[prefix + 'Container']; 10920 offset = rng[prefix + 'Offset']; 10921 10922 if (container.nodeType == 1 && container.nodeName == "TR") { 10923 childNodes = container.childNodes; 10924 container = childNodes[Math.min(start ? offset : offset - 1, childNodes.length - 1)]; 10925 if (container) { 10926 offset = start ? 0 : container.childNodes.length; 10927 rng['set' + (start ? 'Start' : 'End')](container, offset); 10928 } 10929 } 10930 } 10931 10932 moveEndPoint(true); 10933 moveEndPoint(); 10934 10935 return rng; 10936 } 10937 10938 function getLocation() { 10939 var rng = self.getRng(true), root = dom.getRoot(), bookmark = {}; 10940 10941 function getPoint(rng, start) { 10942 var container = rng[start ? 'startContainer' : 'endContainer'], 10943 offset = rng[start ? 'startOffset' : 'endOffset'], point = [], node, childNodes, after = 0; 10944 10945 if (container.nodeType == 3) { 10946 if (normalized) { 10947 for (node = container.previousSibling; node && node.nodeType == 3; node = node.previousSibling) { 10948 offset += node.nodeValue.length; 10949 } 10950 } 10951 10952 point.push(offset); 10953 } else { 10954 childNodes = container.childNodes; 10955 10956 if (offset >= childNodes.length && childNodes.length) { 10957 after = 1; 10958 offset = Math.max(0, childNodes.length - 1); 10959 } 10960 10961 point.push(self.dom.nodeIndex(childNodes[offset], normalized) + after); 10962 } 10963 10964 for (; container && container != root; container = container.parentNode) { 10965 point.push(self.dom.nodeIndex(container, normalized)); 10966 } 10967 10968 return point; 10969 } 10970 10971 bookmark.start = getPoint(rng, true); 10972 10973 if (!self.isCollapsed()) { 10974 bookmark.end = getPoint(rng); 10975 } 10976 10977 return bookmark; 10978 } 10979 10980 if (type == 2) { 10981 element = self.getNode(); 10982 name = element ? element.nodeName : null; 10983 10984 if (name == 'IMG') { 10985 return {name: name, index: findIndex(name, element)}; 10986 } 10987 10988 if (self.tridentSel) { 10989 return self.tridentSel.getBookmark(type); 10990 } 10991 10992 return getLocation(); 10993 } 10994 10995 // Handle simple range 10996 if (type) { 10997 return {rng: self.getRng()}; 10998 } 10999 11000 rng = self.getRng(); 11001 id = dom.uniqueId(); 11002 collapsed = self.isCollapsed(); 11003 styles = 'overflow:hidden;line-height:0px'; 11004 11005 // Explorer method 11006 if (rng.duplicate || rng.item) { 11007 // Text selection 11008 if (!rng.item) { 11009 rng2 = rng.duplicate(); 11010 11011 try { 11012 // Insert start marker 11013 rng.collapse(); 11014 rng.pasteHTML('<span data-mce-type="bookmark" id="' + id + '_start" style="' + styles + '">' + chr + '</span>'); 11015 11016 // Insert end marker 11017 if (!collapsed) { 11018 rng2.collapse(false); 11019 11020 // Detect the empty space after block elements in IE and move the 11021 // end back one character <p></p>] becomes <p>]</p> 11022 rng.moveToElementText(rng2.parentElement()); 11023 if (rng.compareEndPoints('StartToEnd', rng2) === 0) { 11024 rng2.move('character', -1); 11025 } 11026 11027 rng2.pasteHTML('<span data-mce-type="bookmark" id="' + id + '_end" style="' + styles + '">' + chr + '</span>'); 11028 } 11029 } catch (ex) { 11030 // IE might throw unspecified error so lets ignore it 11031 return null; 11032 } 11033 } else { 11034 // Control selection 11035 element = rng.item(0); 11036 name = element.nodeName; 11037 11038 return {name: name, index: findIndex(name, element)}; 11039 } 11040 } else { 11041 element = self.getNode(); 11042 name = element.nodeName; 11043 if (name == 'IMG') { 11044 return {name: name, index: findIndex(name, element)}; 11045 } 11046 11047 // W3C method 11048 rng2 = normalizeTableCellSelection(rng.cloneRange()); 11049 11050 // Insert end marker 11051 if (!collapsed) { 11052 rng2.collapse(false); 11053 rng2.insertNode(dom.create('span', {'data-mce-type': "bookmark", id: id + '_end', style: styles}, chr)); 11054 } 11055 11056 rng = normalizeTableCellSelection(rng); 11057 rng.collapse(true); 11058 rng.insertNode(dom.create('span', {'data-mce-type': "bookmark", id: id + '_start', style: styles}, chr)); 11059 } 11060 11061 self.moveToBookmark({id: id, keep: 1}); 11062 11063 return {id: id}; 11064 }, 11065 11066 /** 11067 * Restores the selection to the specified bookmark. 11068 * 11069 * @method moveToBookmark 11070 * @param {Object} bookmark Bookmark to restore selection from. 11071 * @return {Boolean} true/false if it was successful or not. 11072 * @example 11073 * // Stores a bookmark of the current selection 11074 * var bm = tinymce.activeEditor.selection.getBookmark(); 11075 * 11076 * tinymce.activeEditor.setContent(tinymce.activeEditor.getContent() + 'Some new content'); 11077 * 11078 * // Restore the selection bookmark 11079 * tinymce.activeEditor.selection.moveToBookmark(bm); 11080 */ 11081 moveToBookmark: function(bookmark) { 11082 var self = this, dom = self.dom, rng, root, startContainer, endContainer, startOffset, endOffset; 11083 11084 function setEndPoint(start) { 11085 var point = bookmark[start ? 'start' : 'end'], i, node, offset, children; 11086 11087 if (point) { 11088 offset = point[0]; 11089 11090 // Find container node 11091 for (node = root, i = point.length - 1; i >= 1; i--) { 11092 children = node.childNodes; 11093 11094 if (point[i] > children.length - 1) { 11095 return; 11096 } 11097 11098 node = children[point[i]]; 11099 } 11100 11101 // Move text offset to best suitable location 11102 if (node.nodeType === 3) { 11103 offset = Math.min(point[0], node.nodeValue.length); 11104 } 11105 11106 // Move element offset to best suitable location 11107 if (node.nodeType === 1) { 11108 offset = Math.min(point[0], node.childNodes.length); 11109 } 11110 11111 // Set offset within container node 11112 if (start) { 11113 rng.setStart(node, offset); 11114 } else { 11115 rng.setEnd(node, offset); 11116 } 11117 } 11118 11119 return true; 11120 } 11121 11122 function restoreEndPoint(suffix) { 11123 var marker = dom.get(bookmark.id + '_' + suffix), node, idx, next, prev, keep = bookmark.keep; 11124 11125 if (marker) { 11126 node = marker.parentNode; 11127 11128 if (suffix == 'start') { 11129 if (!keep) { 11130 idx = dom.nodeIndex(marker); 11131 } else { 11132 node = marker.firstChild; 11133 idx = 1; 11134 } 11135 11136 startContainer = endContainer = node; 11137 startOffset = endOffset = idx; 11138 } else { 11139 if (!keep) { 11140 idx = dom.nodeIndex(marker); 11141 } else { 11142 node = marker.firstChild; 11143 idx = 1; 11144 } 11145 11146 endContainer = node; 11147 endOffset = idx; 11148 } 11149 11150 if (!keep) { 11151 prev = marker.previousSibling; 11152 next = marker.nextSibling; 11153 11154 // Remove all marker text nodes 11155 each(grep(marker.childNodes), function(node) { 11156 if (node.nodeType == 3) { 11157 node.nodeValue = node.nodeValue.replace(/\uFEFF/g, ''); 11158 } 11159 }); 11160 11161 // Remove marker but keep children if for example contents where inserted into the marker 11162 // Also remove duplicated instances of the marker for example by a 11163 // split operation or by WebKit auto split on paste feature 11164 while ((marker = dom.get(bookmark.id + '_' + suffix))) { 11165 dom.remove(marker, 1); 11166 } 11167 11168 // If siblings are text nodes then merge them unless it's Opera since it some how removes the node 11169 // and we are sniffing since adding a lot of detection code for a browser with 3% of the market 11170 // isn't worth the effort. Sorry, Opera but it's just a fact 11171 if (prev && next && prev.nodeType == next.nodeType && prev.nodeType == 3 && !isOpera) { 11172 idx = prev.nodeValue.length; 11173 prev.appendData(next.nodeValue); 11174 dom.remove(next); 11175 11176 if (suffix == 'start') { 11177 startContainer = endContainer = prev; 11178 startOffset = endOffset = idx; 11179 } else { 11180 endContainer = prev; 11181 endOffset = idx; 11182 } 11183 } 11184 } 11185 } 11186 } 11187 11188 function addBogus(node) { 11189 // Adds a bogus BR element for empty block elements 11190 if (dom.isBlock(node) && !node.innerHTML && !isIE) { 11191 node.innerHTML = '<br data-mce-bogus="1" />'; 11192 } 11193 11194 return node; 11195 } 11196 11197 if (bookmark) { 11198 if (bookmark.start) { 11199 rng = dom.createRng(); 11200 root = dom.getRoot(); 11201 11202 if (self.tridentSel) { 11203 return self.tridentSel.moveToBookmark(bookmark); 11204 } 11205 11206 if (setEndPoint(true) && setEndPoint()) { 11207 self.setRng(rng); 11208 } 11209 } else if (bookmark.id) { 11210 // Restore start/end points 11211 restoreEndPoint('start'); 11212 restoreEndPoint('end'); 11213 11214 if (startContainer) { 11215 rng = dom.createRng(); 11216 rng.setStart(addBogus(startContainer), startOffset); 11217 rng.setEnd(addBogus(endContainer), endOffset); 11218 self.setRng(rng); 11219 } 11220 } else if (bookmark.name) { 11221 self.select(dom.select(bookmark.name)[bookmark.index]); 11222 } else if (bookmark.rng) { 11223 self.setRng(bookmark.rng); 11224 } 11225 } 11226 }, 11227 11228 /** 11229 * Selects the specified element. This will place the start and end of the selection range around the element. 11230 * 11231 * @method select 11232 * @param {Element} node HMTL DOM element to select. 11233 * @param {Boolean} content Optional bool state if the contents should be selected or not on non IE browser. 11234 * @return {Element} Selected element the same element as the one that got passed in. 11235 * @example 11236 * // Select the first paragraph in the active editor 11237 * tinymce.activeEditor.selection.select(tinymce.activeEditor.dom.select('p')[0]); 11238 */ 11239 select: function(node, content) { 11240 var self = this, dom = self.dom, rng = dom.createRng(), idx; 11241 11242 // Clear stored range set by FocusManager 11243 self.lastFocusBookmark = null; 11244 11245 if (node) { 11246 if (!content && self.controlSelection.controlSelect(node)) { 11247 return; 11248 } 11249 11250 idx = dom.nodeIndex(node); 11251 rng.setStart(node.parentNode, idx); 11252 rng.setEnd(node.parentNode, idx + 1); 11253 11254 // Find first/last text node or BR element 11255 if (content) { 11256 self._moveEndPoint(rng, node, true); 11257 self._moveEndPoint(rng, node); 11258 } 11259 11260 self.setRng(rng); 11261 } 11262 11263 return node; 11264 }, 11265 11266 /** 11267 * Returns true/false if the selection range is collapsed or not. Collapsed means if it's a caret or a larger selection. 11268 * 11269 * @method isCollapsed 11270 * @return {Boolean} true/false state if the selection range is collapsed or not. 11271 * Collapsed means if it's a caret or a larger selection. 11272 */ 11273 isCollapsed: function() { 11274 var self = this, rng = self.getRng(), sel = self.getSel(); 11275 11276 if (!rng || rng.item) { 11277 return false; 11278 } 11279 11280 if (rng.compareEndPoints) { 11281 return rng.compareEndPoints('StartToEnd', rng) === 0; 11282 } 11283 11284 return !sel || rng.collapsed; 11285 }, 11286 11287 /** 11288 * Collapse the selection to start or end of range. 11289 * 11290 * @method collapse 11291 * @param {Boolean} to_start Optional boolean state if to collapse to end or not. Defaults to start. 11292 */ 11293 collapse: function(to_start) { 11294 var self = this, rng = self.getRng(), node; 11295 11296 // Control range on IE 11297 if (rng.item) { 11298 node = rng.item(0); 11299 rng = self.win.document.body.createTextRange(); 11300 rng.moveToElementText(node); 11301 } 11302 11303 rng.collapse(!!to_start); 11304 self.setRng(rng); 11305 }, 11306 11307 /** 11308 * Returns the browsers internal selection object. 11309 * 11310 * @method getSel 11311 * @return {Selection} Internal browser selection object. 11312 */ 11313 getSel: function() { 11314 var win = this.win; 11315 11316 return win.getSelection ? win.getSelection() : win.document.selection; 11317 }, 11318 11319 /** 11320 * Returns the browsers internal range object. 11321 * 11322 * @method getRng 11323 * @param {Boolean} w3c Forces a compatible W3C range on IE. 11324 * @return {Range} Internal browser range object. 11325 * @see http://www.quirksmode.org/dom/range_intro.html 11326 * @see http://www.dotvoid.com/2001/03/using-the-range-object-in-mozilla/ 11327 */ 11328 getRng: function(w3c) { 11329 var self = this, selection, rng, elm, doc = self.win.document, ieRng; 11330 11331 function tryCompareBounderyPoints(how, sourceRange, destinationRange) { 11332 try { 11333 return sourceRange.compareBoundaryPoints(how, destinationRange); 11334 } catch (ex) { 11335 // Gecko throws wrong document exception if the range points 11336 // to nodes that where removed from the dom #6690 11337 // Browsers should mutate existing DOMRange instances so that they always point 11338 // to something in the document this is not the case in Gecko works fine in IE/WebKit/Blink 11339 // For performance reasons just return -1 11340 return -1; 11341 } 11342 } 11343 11344 // Use last rng passed from FocusManager if it's available this enables 11345 // calls to editor.selection.getStart() to work when caret focus is lost on IE 11346 if (!w3c && self.lastFocusBookmark) { 11347 var bookmark = self.lastFocusBookmark; 11348 11349 // Convert bookmark to range IE 11 fix 11350 if (bookmark.startContainer) { 11351 rng = doc.createRange(); 11352 rng.setStart(bookmark.startContainer, bookmark.startOffset); 11353 rng.setEnd(bookmark.endContainer, bookmark.endOffset); 11354 } else { 11355 rng = bookmark; 11356 } 11357 11358 return rng; 11359 } 11360 11361 // Found tridentSel object then we need to use that one 11362 if (w3c && self.tridentSel) { 11363 return self.tridentSel.getRangeAt(0); 11364 } 11365 11366 try { 11367 if ((selection = self.getSel())) { 11368 if (selection.rangeCount > 0) { 11369 rng = selection.getRangeAt(0); 11370 } else { 11371 rng = selection.createRange ? selection.createRange() : doc.createRange(); 11372 } 11373 } 11374 } catch (ex) { 11375 // IE throws unspecified error here if TinyMCE is placed in a frame/iframe 11376 } 11377 11378 // We have W3C ranges and it's IE then fake control selection since IE9 doesn't handle that correctly yet 11379 // IE 11 doesn't support the selection object so we check for that as well 11380 if (isIE && rng && rng.setStart && doc.selection) { 11381 try { 11382 // IE will sometimes throw an exception here 11383 ieRng = doc.selection.createRange(); 11384 } catch (ex) { 11385 11386 } 11387 11388 if (ieRng && ieRng.item) { 11389 elm = ieRng.item(0); 11390 rng = doc.createRange(); 11391 rng.setStartBefore(elm); 11392 rng.setEndAfter(elm); 11393 } 11394 } 11395 11396 // No range found then create an empty one 11397 // This can occur when the editor is placed in a hidden container element on Gecko 11398 // Or on IE when there was an exception 11399 if (!rng) { 11400 rng = doc.createRange ? doc.createRange() : doc.body.createTextRange(); 11401 } 11402 11403 // If range is at start of document then move it to start of body 11404 if (rng.setStart && rng.startContainer.nodeType === 9 && rng.collapsed) { 11405 elm = self.dom.getRoot(); 11406 rng.setStart(elm, 0); 11407 rng.setEnd(elm, 0); 11408 } 11409 11410 if (self.selectedRange && self.explicitRange) { 11411 if (tryCompareBounderyPoints(rng.START_TO_START, rng, self.selectedRange) === 0 && 11412 tryCompareBounderyPoints(rng.END_TO_END, rng, self.selectedRange) === 0) { 11413 // Safari, Opera and Chrome only ever select text which causes the range to change. 11414 // This lets us use the originally set range if the selection hasn't been changed by the user. 11415 rng = self.explicitRange; 11416 } else { 11417 self.selectedRange = null; 11418 self.explicitRange = null; 11419 } 11420 } 11421 11422 return rng; 11423 }, 11424 11425 /** 11426 * Changes the selection to the specified DOM range. 11427 * 11428 * @method setRng 11429 * @param {Range} rng Range to select. 11430 */ 11431 setRng: function(rng, forward) { 11432 var self = this, sel; 11433 11434 // Is IE specific range 11435 if (rng.select) { 11436 try { 11437 rng.select(); 11438 } catch (ex) { 11439 // Needed for some odd IE bug #1843306 11440 } 11441 11442 return; 11443 } 11444 11445 if (!self.tridentSel) { 11446 sel = self.getSel(); 11447 11448 if (sel) { 11449 self.explicitRange = rng; 11450 11451 try { 11452 sel.removeAllRanges(); 11453 sel.addRange(rng); 11454 } catch (ex) { 11455 // IE might throw errors here if the editor is within a hidden container and selection is changed 11456 } 11457 11458 // Forward is set to false and we have an extend function 11459 if (forward === false && sel.extend) { 11460 sel.collapse(rng.endContainer, rng.endOffset); 11461 sel.extend(rng.startContainer, rng.startOffset); 11462 } 11463 11464 // adding range isn't always successful so we need to check range count otherwise an exception can occur 11465 self.selectedRange = sel.rangeCount > 0 ? sel.getRangeAt(0) : null; 11466 } 11467 } else { 11468 // Is W3C Range fake range on IE 11469 if (rng.cloneRange) { 11470 try { 11471 self.tridentSel.addRange(rng); 11472 return; 11473 } catch (ex) { 11474 //IE9 throws an error here if called before selection is placed in the editor 11475 } 11476 } 11477 } 11478 }, 11479 11480 /** 11481 * Sets the current selection to the specified DOM element. 11482 * 11483 * @method setNode 11484 * @param {Element} elm Element to set as the contents of the selection. 11485 * @return {Element} Returns the element that got passed in. 11486 * @example 11487 * // Inserts a DOM node at current selection/caret location 11488 * tinymce.activeEditor.selection.setNode(tinymce.activeEditor.dom.create('img', {src: 'some.gif', title: 'some title'})); 11489 */ 11490 setNode: function(elm) { 11491 var self = this; 11492 11493 self.setContent(self.dom.getOuterHTML(elm)); 11494 11495 return elm; 11496 }, 11497 11498 /** 11499 * Returns the currently selected element or the common ancestor element for both start and end of the selection. 11500 * 11501 * @method getNode 11502 * @return {Element} Currently selected element or common ancestor element. 11503 * @example 11504 * // Alerts the currently selected elements node name 11505 * alert(tinymce.activeEditor.selection.getNode().nodeName); 11506 */ 11507 getNode: function() { 11508 var self = this, rng = self.getRng(), elm; 11509 var startContainer = rng.startContainer, endContainer = rng.endContainer; 11510 var startOffset = rng.startOffset, endOffset = rng.endOffset, root = self.dom.getRoot(); 11511 11512 function skipEmptyTextNodes(node, forwards) { 11513 var orig = node; 11514 11515 while (node && node.nodeType === 3 && node.length === 0) { 11516 node = forwards ? node.nextSibling : node.previousSibling; 11517 } 11518 11519 return node || orig; 11520 } 11521 11522 // Range maybe lost after the editor is made visible again 11523 if (!rng) { 11524 return root; 11525 } 11526 11527 if (rng.setStart) { 11528 elm = rng.commonAncestorContainer; 11529 11530 // Handle selection a image or other control like element such as anchors 11531 if (!rng.collapsed) { 11532 if (startContainer == endContainer) { 11533 if (endOffset - startOffset < 2) { 11534 if (startContainer.hasChildNodes()) { 11535 elm = startContainer.childNodes[startOffset]; 11536 } 11537 } 11538 } 11539 11540 // If the anchor node is a element instead of a text node then return this element 11541 //if (tinymce.isWebKit && sel.anchorNode && sel.anchorNode.nodeType == 1) 11542 // return sel.anchorNode.childNodes[sel.anchorOffset]; 11543 11544 // Handle cases where the selection is immediately wrapped around a node and return that node instead of it's parent. 11545 // This happens when you double click an underlined word in FireFox. 11546 if (startContainer.nodeType === 3 && endContainer.nodeType === 3) { 11547 if (startContainer.length === startOffset) { 11548 startContainer = skipEmptyTextNodes(startContainer.nextSibling, true); 11549 } else { 11550 startContainer = startContainer.parentNode; 11551 } 11552 11553 if (endOffset === 0) { 11554 endContainer = skipEmptyTextNodes(endContainer.previousSibling, false); 11555 } else { 11556 endContainer = endContainer.parentNode; 11557 } 11558 11559 if (startContainer && startContainer === endContainer) { 11560 return startContainer; 11561 } 11562 } 11563 } 11564 11565 if (elm && elm.nodeType == 3) { 11566 return elm.parentNode; 11567 } 11568 11569 return elm; 11570 } 11571 11572 elm = rng.item ? rng.item(0) : rng.parentElement(); 11573 11574 // IE 7 might return elements outside the iframe 11575 if (elm.ownerDocument !== self.win.document) { 11576 elm = root; 11577 } 11578 11579 return elm; 11580 }, 11581 11582 getSelectedBlocks: function(startElm, endElm) { 11583 var self = this, dom = self.dom, node, root, selectedBlocks = []; 11584 11585 root = dom.getRoot(); 11586 startElm = dom.getParent(startElm || self.getStart(), dom.isBlock); 11587 endElm = dom.getParent(endElm || self.getEnd(), dom.isBlock); 11588 11589 if (startElm && startElm != root) { 11590 selectedBlocks.push(startElm); 11591 } 11592 11593 if (startElm && endElm && startElm != endElm) { 11594 node = startElm; 11595 11596 var walker = new TreeWalker(startElm, root); 11597 while ((node = walker.next()) && node != endElm) { 11598 if (dom.isBlock(node)) { 11599 selectedBlocks.push(node); 11600 } 11601 } 11602 } 11603 11604 if (endElm && startElm != endElm && endElm != root) { 11605 selectedBlocks.push(endElm); 11606 } 11607 11608 return selectedBlocks; 11609 }, 11610 11611 isForward: function() { 11612 var dom = this.dom, sel = this.getSel(), anchorRange, focusRange; 11613 11614 // No support for selection direction then always return true 11615 if (!sel || !sel.anchorNode || !sel.focusNode) { 11616 return true; 11617 } 11618 11619 anchorRange = dom.createRng(); 11620 anchorRange.setStart(sel.anchorNode, sel.anchorOffset); 11621 anchorRange.collapse(true); 11622 11623 focusRange = dom.createRng(); 11624 focusRange.setStart(sel.focusNode, sel.focusOffset); 11625 focusRange.collapse(true); 11626 11627 return anchorRange.compareBoundaryPoints(anchorRange.START_TO_START, focusRange) <= 0; 11628 }, 11629 11630 normalize: function() { 11631 var self = this, rng = self.getRng(); 11632 11633 if (!isIE && new RangeUtils(self.dom).normalize(rng)) { 11634 self.setRng(rng, self.isForward()); 11635 } 11636 11637 return rng; 11638 }, 11639 11640 /** 11641 * Executes callback of the current selection matches the specified selector or not and passes the state and args to the callback. 11642 * 11643 * @method selectorChanged 11644 * @param {String} selector CSS selector to check for. 11645 * @param {function} callback Callback with state and args when the selector is matches or not. 11646 */ 11647 selectorChanged: function(selector, callback) { 11648 var self = this, currentSelectors; 11649 11650 if (!self.selectorChangedData) { 11651 self.selectorChangedData = {}; 11652 currentSelectors = {}; 11653 11654 self.editor.on('NodeChange', function(e) { 11655 var node = e.element, dom = self.dom, parents = dom.getParents(node, null, dom.getRoot()), matchedSelectors = {}; 11656 11657 // Check for new matching selectors 11658 each(self.selectorChangedData, function(callbacks, selector) { 11659 each(parents, function(node) { 11660 if (dom.is(node, selector)) { 11661 if (!currentSelectors[selector]) { 11662 // Execute callbacks 11663 each(callbacks, function(callback) { 11664 callback(true, {node: node, selector: selector, parents: parents}); 11665 }); 11666 11667 currentSelectors[selector] = callbacks; 11668 } 11669 11670 matchedSelectors[selector] = callbacks; 11671 return false; 11672 } 11673 }); 11674 }); 11675 11676 // Check if current selectors still match 11677 each(currentSelectors, function(callbacks, selector) { 11678 if (!matchedSelectors[selector]) { 11679 delete currentSelectors[selector]; 11680 11681 each(callbacks, function(callback) { 11682 callback(false, {node: node, selector: selector, parents: parents}); 11683 }); 11684 } 11685 }); 11686 }); 11687 } 11688 11689 // Add selector listeners 11690 if (!self.selectorChangedData[selector]) { 11691 self.selectorChangedData[selector] = []; 11692 } 11693 11694 self.selectorChangedData[selector].push(callback); 11695 11696 return self; 11697 }, 11698 11699 getScrollContainer: function() { 11700 var scrollContainer, node = this.dom.getRoot(); 11701 11702 while (node && node.nodeName != 'BODY') { 11703 if (node.scrollHeight > node.clientHeight) { 11704 scrollContainer = node; 11705 break; 11706 } 11707 11708 node = node.parentNode; 11709 } 11710 11711 return scrollContainer; 11712 }, 11713 11714 scrollIntoView: function(elm) { 11715 var y, viewPort, self = this, dom = self.dom, root = dom.getRoot(), viewPortY, viewPortH; 11716 11717 function getPos(elm) { 11718 var x = 0, y = 0; 11719 11720 var offsetParent = elm; 11721 while (offsetParent && offsetParent.nodeType) { 11722 x += offsetParent.offsetLeft || 0; 11723 y += offsetParent.offsetTop || 0; 11724 offsetParent = offsetParent.offsetParent; 11725 } 11726 11727 return {x: x, y: y}; 11728 } 11729 11730 if (root.nodeName != 'BODY') { 11731 var scrollContainer = self.getScrollContainer(); 11732 if (scrollContainer) { 11733 y = getPos(elm).y - getPos(scrollContainer).y; 11734 viewPortH = scrollContainer.clientHeight; 11735 viewPortY = scrollContainer.scrollTop; 11736 if (y < viewPortY || y + 25 > viewPortY + viewPortH) { 11737 scrollContainer.scrollTop = y < viewPortY ? y : y - viewPortH + 25; 11738 } 11739 11740 return; 11741 } 11742 } 11743 11744 viewPort = dom.getViewPort(self.editor.getWin()); 11745 y = dom.getPos(elm).y; 11746 viewPortY = viewPort.y; 11747 viewPortH = viewPort.h; 11748 if (y < viewPort.y || y + 25 > viewPortY + viewPortH) { 11749 self.editor.getWin().scrollTo(0, y < viewPortY ? y : y - viewPortH + 25); 11750 } 11751 }, 11752 11753 _moveEndPoint: function(rng, node, start) { 11754 var root = node, walker = new TreeWalker(node, root); 11755 var nonEmptyElementsMap = this.dom.schema.getNonEmptyElements(); 11756 11757 do { 11758 // Text node 11759 if (node.nodeType == 3 && trim(node.nodeValue).length !== 0) { 11760 if (start) { 11761 rng.setStart(node, 0); 11762 } else { 11763 rng.setEnd(node, node.nodeValue.length); 11764 } 11765 11766 return; 11767 } 11768 11769 // BR/IMG/INPUT elements 11770 if (nonEmptyElementsMap[node.nodeName]) { 11771 if (start) { 11772 rng.setStartBefore(node); 11773 } else { 11774 if (node.nodeName == 'BR') { 11775 rng.setEndBefore(node); 11776 } else { 11777 rng.setEndAfter(node); 11778 } 11779 } 11780 11781 return; 11782 } 11783 11784 // Found empty text block old IE can place the selection inside those 11785 if (Env.ie && Env.ie < 11 && this.dom.isBlock(node) && this.dom.isEmpty(node)) { 11786 if (start) { 11787 rng.setStart(node, 0); 11788 } else { 11789 rng.setEnd(node, 0); 11790 } 11791 11792 return; 11793 } 11794 } while ((node = (start ? walker.next() : walker.prev()))); 11795 11796 // Failed to find any text node or other suitable location then move to the root of body 11797 if (root.nodeName == 'BODY') { 11798 if (start) { 11799 rng.setStart(root, 0); 11800 } else { 11801 rng.setEnd(root, root.childNodes.length); 11802 } 11803 } 11804 }, 11805 11806 destroy: function() { 11807 this.win = null; 11808 this.controlSelection.destroy(); 11809 } 11810 }; 11811 11812 return Selection; 11813 }); 11814 11815 // Included from: js/tinymce/classes/fmt/Preview.js 11816 11817 /** 11818 * Preview.js 11819 * 11820 * Copyright, Moxiecode Systems AB 11821 * Released under LGPL License. 11822 * 11823 * License: http://www.tinymce.com/license 11824 * Contributing: http://www.tinymce.com/contributing 11825 */ 11826 11827 /** 11828 * Internal class for generating previews styles for formats. 11829 * 11830 * Example: 11831 * Preview.getCssText(editor, 'bold'); 11832 * 11833 * @class tinymce.fmt.Preview 11834 * @private 11835 */ 11836 define("tinymce/fmt/Preview", [ 11837 "tinymce/util/Tools" 11838 ], function(Tools) { 11839 var each = Tools.each; 11840 11841 function getCssText(editor, format) { 11842 var name, previewElm, dom = editor.dom; 11843 var previewCss = '', parentFontSize, previewStyles; 11844 11845 previewStyles = editor.settings.preview_styles; 11846 11847 // No preview forced 11848 if (previewStyles === false) { 11849 return ''; 11850 } 11851 11852 // Default preview 11853 if (!previewStyles) { 11854 previewStyles = 'font-family font-size font-weight font-style text-decoration ' + 11855 'text-transform color background-color border border-radius outline text-shadow'; 11856 } 11857 11858 // Removes any variables since these can't be previewed 11859 function removeVars(val) { 11860 return val.replace(/%(\w+)/g, ''); 11861 } 11862 11863 // Create block/inline element to use for preview 11864 if (typeof(format) == "string") { 11865 format = editor.formatter.get(format); 11866 if (!format) { 11867 return; 11868 } 11869 11870 format = format[0]; 11871 } 11872 11873 name = format.block || format.inline || 'span'; 11874 previewElm = dom.create(name); 11875 11876 // Add format styles to preview element 11877 each(format.styles, function(value, name) { 11878 value = removeVars(value); 11879 11880 if (value) { 11881 dom.setStyle(previewElm, name, value); 11882 } 11883 }); 11884 11885 // Add attributes to preview element 11886 each(format.attributes, function(value, name) { 11887 value = removeVars(value); 11888 11889 if (value) { 11890 dom.setAttrib(previewElm, name, value); 11891 } 11892 }); 11893 11894 // Add classes to preview element 11895 each(format.classes, function(value) { 11896 value = removeVars(value); 11897 11898 if (!dom.hasClass(previewElm, value)) { 11899 dom.addClass(previewElm, value); 11900 } 11901 }); 11902 11903 editor.fire('PreviewFormats'); 11904 11905 // Add the previewElm outside the visual area 11906 dom.setStyles(previewElm, {position: 'absolute', left: -0xFFFF}); 11907 editor.getBody().appendChild(previewElm); 11908 11909 // Get parent container font size so we can compute px values out of em/% for older IE:s 11910 parentFontSize = dom.getStyle(editor.getBody(), 'fontSize', true); 11911 parentFontSize = /px$/.test(parentFontSize) ? parseInt(parentFontSize, 10) : 0; 11912 11913 each(previewStyles.split(' '), function(name) { 11914 var value = dom.getStyle(previewElm, name, true); 11915 11916 // If background is transparent then check if the body has a background color we can use 11917 if (name == 'background-color' && /transparent|rgba\s*\([^)]+,\s*0\)/.test(value)) { 11918 value = dom.getStyle(editor.getBody(), name, true); 11919 11920 // Ignore white since it's the default color, not the nicest fix 11921 // TODO: Fix this by detecting runtime style 11922 if (dom.toHex(value).toLowerCase() == '#ffffff') { 11923 return; 11924 } 11925 } 11926 11927 if (name == 'color') { 11928 // Ignore black since it's the default color, not the nicest fix 11929 // TODO: Fix this by detecting runtime style 11930 if (dom.toHex(value).toLowerCase() == '#000000') { 11931 return; 11932 } 11933 } 11934 11935 // Old IE won't calculate the font size so we need to do that manually 11936 if (name == 'font-size') { 11937 if (/em|%$/.test(value)) { 11938 if (parentFontSize === 0) { 11939 return; 11940 } 11941 11942 // Convert font size from em/% to px 11943 value = parseFloat(value, 10) / (/%$/.test(value) ? 100 : 1); 11944 value = (value * parentFontSize) + 'px'; 11945 } 11946 } 11947 11948 if (name == "border" && value) { 11949 previewCss += 'padding:0 2px;'; 11950 } 11951 11952 previewCss += name + ':' + value + ';'; 11953 }); 11954 11955 editor.fire('AfterPreviewFormats'); 11956 11957 //previewCss += 'line-height:normal'; 11958 11959 dom.remove(previewElm); 11960 11961 return previewCss; 11962 } 11963 11964 return { 11965 getCssText: getCssText 11966 }; 11967 }); 11968 11969 // Included from: js/tinymce/classes/Formatter.js 11970 11971 /** 11972 * Formatter.js 11973 * 11974 * Copyright, Moxiecode Systems AB 11975 * Released under LGPL License. 11976 * 11977 * License: http://www.tinymce.com/license 11978 * Contributing: http://www.tinymce.com/contributing 11979 */ 11980 11981 /** 11982 * Text formatter engine class. This class is used to apply formats like bold, italic, font size 11983 * etc to the current selection or specific nodes. This engine was build to replace the browsers 11984 * default formatting logic for execCommand due to it's inconsistent and buggy behavior. 11985 * 11986 * @class tinymce.Formatter 11987 * @example 11988 * tinymce.activeEditor.formatter.register('mycustomformat', { 11989 * inline: 'span', 11990 * styles: {color: '#ff0000'} 11991 * }); 11992 * 11993 * tinymce.activeEditor.formatter.apply('mycustomformat'); 11994 */ 11995 define("tinymce/Formatter", [ 11996 "tinymce/dom/TreeWalker", 11997 "tinymce/dom/RangeUtils", 11998 "tinymce/util/Tools", 11999 "tinymce/fmt/Preview" 12000 ], function(TreeWalker, RangeUtils, Tools, Preview) { 12001 /** 12002 * Constructs a new formatter instance. 12003 * 12004 * @constructor Formatter 12005 * @param {tinymce.Editor} ed Editor instance to construct the formatter engine to. 12006 */ 12007 return function(ed) { 12008 var formats = {}, 12009 dom = ed.dom, 12010 selection = ed.selection, 12011 rangeUtils = new RangeUtils(dom), 12012 isValid = ed.schema.isValidChild, 12013 isBlock = dom.isBlock, 12014 forcedRootBlock = ed.settings.forced_root_block, 12015 nodeIndex = dom.nodeIndex, 12016 INVISIBLE_CHAR = '\uFEFF', 12017 MCE_ATTR_RE = /^(src|href|style)$/, 12018 FALSE = false, 12019 TRUE = true, 12020 formatChangeData, 12021 undef, 12022 getContentEditable = dom.getContentEditable, 12023 disableCaretContainer, 12024 markCaretContainersBogus; 12025 12026 var each = Tools.each, 12027 grep = Tools.grep, 12028 walk = Tools.walk, 12029 extend = Tools.extend; 12030 12031 function isTextBlock(name) { 12032 if (name.nodeType) { 12033 name = name.nodeName; 12034 } 12035 12036 return !!ed.schema.getTextBlockElements()[name.toLowerCase()]; 12037 } 12038 12039 function getParents(node, selector) { 12040 return dom.getParents(node, selector, dom.getRoot()); 12041 } 12042 12043 function isCaretNode(node) { 12044 return node.nodeType === 1 && node.id === '_mce_caret'; 12045 } 12046 12047 function defaultFormats() { 12048 register({ 12049 12050 valigntop: [ 12051 {selector: 'td,th', styles: {'verticalAlign': 'top'}} 12052 ], 12053 12054 valignmiddle: [ 12055 {selector: 'td,th', styles: {'verticalAlign': 'middle'}} 12056 ], 12057 12058 valignbottom: [ 12059 {selector: 'td,th', styles: {'verticalAlign': 'bottom'}} 12060 ], 12061 12062 alignleft: [ 12063 {selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li', styles: {textAlign: 'left'}, defaultBlock: 'div'}, 12064 {selector: 'img,table', collapsed: false, styles: {'float': 'left'}} 12065 ], 12066 12067 aligncenter: [ 12068 {selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li', styles: {textAlign: 'center'}, defaultBlock: 'div'}, 12069 {selector: 'img', collapsed: false, styles: {display: 'block', marginLeft: 'auto', marginRight: 'auto'}}, 12070 {selector: 'table', collapsed: false, styles: {marginLeft: 'auto', marginRight: 'auto'}} 12071 ], 12072 12073 alignright: [ 12074 {selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li', styles: {textAlign: 'right'}, defaultBlock: 'div'}, 12075 {selector: 'img,table', collapsed: false, styles: {'float': 'right'}} 12076 ], 12077 12078 alignjustify: [ 12079 {selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li', styles: {textAlign: 'justify'}, defaultBlock: 'div'} 12080 ], 12081 12082 bold: [ 12083 {inline: 'strong', remove: 'all'}, 12084 {inline: 'span', styles: {fontWeight: 'bold'}}, 12085 {inline: 'b', remove: 'all'} 12086 ], 12087 12088 italic: [ 12089 {inline: 'em', remove: 'all'}, 12090 {inline: 'span', styles: {fontStyle: 'italic'}}, 12091 {inline: 'i', remove: 'all'} 12092 ], 12093 12094 underline: [ 12095 {inline: 'span', styles: {textDecoration: 'underline'}, exact: true}, 12096 {inline: 'u', remove: 'all'} 12097 ], 12098 12099 strikethrough: [ 12100 {inline: 'span', styles: {textDecoration: 'line-through'}, exact: true}, 12101 {inline: 'strike', remove: 'all'} 12102 ], 12103 12104 forecolor: {inline: 'span', styles: {color: '%value'}, wrap_links: false}, 12105 hilitecolor: {inline: 'span', styles: {backgroundColor: '%value'}, wrap_links: false}, 12106 fontname: {inline: 'span', styles: {fontFamily: '%value'}}, 12107 fontsize: {inline: 'span', styles: {fontSize: '%value'}}, 12108 fontsize_class: {inline: 'span', attributes: {'class': '%value'}}, 12109 blockquote: {block: 'blockquote', wrapper: 1, remove: 'all'}, 12110 subscript: {inline: 'sub'}, 12111 superscript: {inline: 'sup'}, 12112 code: {inline: 'code'}, 12113 12114 link: {inline: 'a', selector: 'a', remove: 'all', split: true, deep: true, 12115 onmatch: function() { 12116 return true; 12117 }, 12118 12119 onformat: function(elm, fmt, vars) { 12120 each(vars, function(value, key) { 12121 dom.setAttrib(elm, key, value); 12122 }); 12123 } 12124 }, 12125 12126 removeformat: [ 12127 { 12128 selector: 'b,strong,em,i,font,u,strike,sub,sup,dfn,code,samp,kbd,var,cite,mark,q', 12129 remove: 'all', 12130 split: true, 12131 expand: false, 12132 block_expand: true, 12133 deep: true 12134 }, 12135 {selector: 'span', attributes: ['style', 'class'], remove: 'empty', split: true, expand: false, deep: true}, 12136 {selector: '*', attributes: ['style', 'class'], split: false, expand: false, deep: true} 12137 ] 12138 }); 12139 12140 // Register default block formats 12141 each('p h1 h2 h3 h4 h5 h6 div address pre div dt dd samp'.split(/\s/), function(name) { 12142 register(name, {block: name, remove: 'all'}); 12143 }); 12144 12145 // Register user defined formats 12146 register(ed.settings.formats); 12147 } 12148 12149 function addKeyboardShortcuts() { 12150 // Add some inline shortcuts 12151 ed.addShortcut('ctrl+b', 'bold_desc', 'Bold'); 12152 ed.addShortcut('ctrl+i', 'italic_desc', 'Italic'); 12153 ed.addShortcut('ctrl+u', 'underline_desc', 'Underline'); 12154 12155 // BlockFormat shortcuts keys 12156 for (var i = 1; i <= 6; i++) { 12157 ed.addShortcut('ctrl+' + i, '', ['FormatBlock', false, 'h' + i]); 12158 } 12159 12160 ed.addShortcut('ctrl+7', '', ['FormatBlock', false, 'p']); 12161 ed.addShortcut('ctrl+8', '', ['FormatBlock', false, 'div']); 12162 ed.addShortcut('ctrl+9', '', ['FormatBlock', false, 'address']); 12163 } 12164 12165 // Public functions 12166 12167 /** 12168 * Returns the format by name or all formats if no name is specified. 12169 * 12170 * @method get 12171 * @param {String} name Optional name to retrive by. 12172 * @return {Array/Object} Array/Object with all registred formats or a specific format. 12173 */ 12174 function get(name) { 12175 return name ? formats[name] : formats; 12176 } 12177 12178 /** 12179 * Registers a specific format by name. 12180 * 12181 * @method register 12182 * @param {Object/String} name Name of the format for example "bold". 12183 * @param {Object/Array} format Optional format object or array of format variants 12184 * can only be omitted if the first arg is an object. 12185 */ 12186 function register(name, format) { 12187 if (name) { 12188 if (typeof(name) !== 'string') { 12189 each(name, function(format, name) { 12190 register(name, format); 12191 }); 12192 } else { 12193 // Force format into array and add it to internal collection 12194 format = format.length ? format : [format]; 12195 12196 each(format, function(format) { 12197 // Set deep to false by default on selector formats this to avoid removing 12198 // alignment on images inside paragraphs when alignment is changed on paragraphs 12199 if (format.deep === undef) { 12200 format.deep = !format.selector; 12201 } 12202 12203 // Default to true 12204 if (format.split === undef) { 12205 format.split = !format.selector || format.inline; 12206 } 12207 12208 // Default to true 12209 if (format.remove === undef && format.selector && !format.inline) { 12210 format.remove = 'none'; 12211 } 12212 12213 // Mark format as a mixed format inline + block level 12214 if (format.selector && format.inline) { 12215 format.mixed = true; 12216 format.block_expand = true; 12217 } 12218 12219 // Split classes if needed 12220 if (typeof(format.classes) === 'string') { 12221 format.classes = format.classes.split(/\s+/); 12222 } 12223 }); 12224 12225 formats[name] = format; 12226 } 12227 } 12228 } 12229 12230 function getTextDecoration(node) { 12231 var decoration; 12232 12233 ed.dom.getParent(node, function(n) { 12234 decoration = ed.dom.getStyle(n, 'text-decoration'); 12235 return decoration && decoration !== 'none'; 12236 }); 12237 12238 return decoration; 12239 } 12240 12241 function processUnderlineAndColor(node) { 12242 var textDecoration; 12243 if (node.nodeType === 1 && node.parentNode && node.parentNode.nodeType === 1) { 12244 textDecoration = getTextDecoration(node.parentNode); 12245 if (ed.dom.getStyle(node, 'color') && textDecoration) { 12246 ed.dom.setStyle(node, 'text-decoration', textDecoration); 12247 } else if (ed.dom.getStyle(node, 'textdecoration') === textDecoration) { 12248 ed.dom.setStyle(node, 'text-decoration', null); 12249 } 12250 } 12251 } 12252 12253 /** 12254 * Applies the specified format to the current selection or specified node. 12255 * 12256 * @method apply 12257 * @param {String} name Name of format to apply. 12258 * @param {Object} vars Optional list of variables to replace within format before applying it. 12259 * @param {Node} node Optional node to apply the format to defaults to current selection. 12260 */ 12261 function apply(name, vars, node) { 12262 var formatList = get(name), format = formatList[0], bookmark, rng, isCollapsed = !node && selection.isCollapsed(); 12263 12264 function setElementFormat(elm, fmt) { 12265 fmt = fmt || format; 12266 12267 if (elm) { 12268 if (fmt.onformat) { 12269 fmt.onformat(elm, fmt, vars, node); 12270 } 12271 12272 each(fmt.styles, function(value, name) { 12273 dom.setStyle(elm, name, replaceVars(value, vars)); 12274 }); 12275 12276 // Needed for the WebKit span spam bug 12277 // TODO: Remove this once WebKit/Blink fixes this 12278 if (fmt.styles) { 12279 var styleVal = dom.getAttrib(elm, 'style'); 12280 12281 if (styleVal) { 12282 elm.setAttribute('data-mce-style', styleVal); 12283 } 12284 } 12285 12286 each(fmt.attributes, function(value, name) { 12287 dom.setAttrib(elm, name, replaceVars(value, vars)); 12288 }); 12289 12290 each(fmt.classes, function(value) { 12291 value = replaceVars(value, vars); 12292 12293 if (!dom.hasClass(elm, value)) { 12294 dom.addClass(elm, value); 12295 } 12296 }); 12297 } 12298 } 12299 12300 function adjustSelectionToVisibleSelection() { 12301 function findSelectionEnd(start, end) { 12302 var walker = new TreeWalker(end); 12303 for (node = walker.current(); node; node = walker.prev()) { 12304 if (node.childNodes.length > 1 || node == start || node.tagName == 'BR') { 12305 return node; 12306 } 12307 } 12308 } 12309 12310 // Adjust selection so that a end container with a end offset of zero is not included in the selection 12311 // as this isn't visible to the user. 12312 var rng = ed.selection.getRng(); 12313 var start = rng.startContainer; 12314 var end = rng.endContainer; 12315 12316 if (start != end && rng.endOffset === 0) { 12317 var newEnd = findSelectionEnd(start, end); 12318 var endOffset = newEnd.nodeType == 3 ? newEnd.length : newEnd.childNodes.length; 12319 12320 rng.setEnd(newEnd, endOffset); 12321 } 12322 12323 return rng; 12324 } 12325 12326 function applyStyleToList(node, bookmark, wrapElm, newWrappers, process){ 12327 var nodes = [], listIndex = -1, list, startIndex = -1, endIndex = -1, currentWrapElm; 12328 12329 // find the index of the first child list. 12330 each(node.childNodes, function(n, index) { 12331 if (n.nodeName === "UL" || n.nodeName === "OL") { 12332 listIndex = index; 12333 list = n; 12334 return false; 12335 } 12336 }); 12337 12338 // get the index of the bookmarks 12339 each(node.childNodes, function(n, index) { 12340 if (n.nodeName === "SPAN" && dom.getAttrib(n, "data-mce-type") == "bookmark") { 12341 if (n.id == bookmark.id + "_start") { 12342 startIndex = index; 12343 } else if (n.id == bookmark.id + "_end") { 12344 endIndex = index; 12345 } 12346 } 12347 }); 12348 12349 // if the selection spans across an embedded list, or there isn't an embedded list - handle processing normally 12350 if (listIndex <= 0 || (startIndex < listIndex && endIndex > listIndex)) { 12351 each(grep(node.childNodes), process); 12352 return 0; 12353 } else { 12354 currentWrapElm = dom.clone(wrapElm, FALSE); 12355 12356 // create a list of the nodes on the same side of the list as the selection 12357 each(grep(node.childNodes), function(n, index) { 12358 if ((startIndex < listIndex && index < listIndex) || (startIndex > listIndex && index > listIndex)) { 12359 nodes.push(n); 12360 n.parentNode.removeChild(n); 12361 } 12362 }); 12363 12364 // insert the wrapping element either before or after the list. 12365 if (startIndex < listIndex) { 12366 node.insertBefore(currentWrapElm, list); 12367 } else if (startIndex > listIndex) { 12368 node.insertBefore(currentWrapElm, list.nextSibling); 12369 } 12370 12371 // add the new nodes to the list. 12372 newWrappers.push(currentWrapElm); 12373 12374 each(nodes, function(node) { 12375 currentWrapElm.appendChild(node); 12376 }); 12377 12378 return currentWrapElm; 12379 } 12380 } 12381 12382 function applyRngStyle(rng, bookmark, node_specific) { 12383 var newWrappers = [], wrapName, wrapElm, contentEditable = true; 12384 12385 // Setup wrapper element 12386 wrapName = format.inline || format.block; 12387 wrapElm = dom.create(wrapName); 12388 setElementFormat(wrapElm); 12389 12390 rangeUtils.walk(rng, function(nodes) { 12391 var currentWrapElm; 12392 12393 /** 12394 * Process a list of nodes wrap them. 12395 */ 12396 function process(node) { 12397 var nodeName, parentName, found, hasContentEditableState, lastContentEditable; 12398 12399 lastContentEditable = contentEditable; 12400 nodeName = node.nodeName.toLowerCase(); 12401 parentName = node.parentNode.nodeName.toLowerCase(); 12402 12403 // Node has a contentEditable value 12404 if (node.nodeType === 1 && getContentEditable(node)) { 12405 lastContentEditable = contentEditable; 12406 contentEditable = getContentEditable(node) === "true"; 12407 hasContentEditableState = true; // We don't want to wrap the container only it's children 12408 } 12409 12410 // Stop wrapping on br elements 12411 if (isEq(nodeName, 'br')) { 12412 currentWrapElm = 0; 12413 12414 // Remove any br elements when we wrap things 12415 if (format.block) { 12416 dom.remove(node); 12417 } 12418 12419 return; 12420 } 12421 12422 // If node is wrapper type 12423 if (format.wrapper && matchNode(node, name, vars)) { 12424 currentWrapElm = 0; 12425 return; 12426 } 12427 12428 // Can we rename the block 12429 // TODO: Break this if up, too complex 12430 if (contentEditable && !hasContentEditableState && format.block && 12431 !format.wrapper && isTextBlock(nodeName) && isValid(parentName, wrapName)) { 12432 node = dom.rename(node, wrapName); 12433 setElementFormat(node); 12434 newWrappers.push(node); 12435 currentWrapElm = 0; 12436 return; 12437 } 12438 12439 // Handle selector patterns 12440 if (format.selector) { 12441 // Look for matching formats 12442 each(formatList, function(format) { 12443 // Check collapsed state if it exists 12444 if ('collapsed' in format && format.collapsed !== isCollapsed) { 12445 return; 12446 } 12447 12448 if (dom.is(node, format.selector) && !isCaretNode(node)) { 12449 setElementFormat(node, format); 12450 found = true; 12451 } 12452 }); 12453 12454 // Continue processing if a selector match wasn't found and a inline element is defined 12455 if (!format.inline || found) { 12456 currentWrapElm = 0; 12457 return; 12458 } 12459 } 12460 12461 // Is it valid to wrap this item 12462 // TODO: Break this if up, too complex 12463 if (contentEditable && !hasContentEditableState && isValid(wrapName, nodeName) && isValid(parentName, wrapName) && 12464 !(!node_specific && node.nodeType === 3 && 12465 node.nodeValue.length === 1 && 12466 node.nodeValue.charCodeAt(0) === 65279) && 12467 !isCaretNode(node) && 12468 (!format.inline || !isBlock(node))) { 12469 // Start wrapping 12470 if (!currentWrapElm) { 12471 // Wrap the node 12472 currentWrapElm = dom.clone(wrapElm, FALSE); 12473 node.parentNode.insertBefore(currentWrapElm, node); 12474 newWrappers.push(currentWrapElm); 12475 } 12476 12477 currentWrapElm.appendChild(node); 12478 } else if (nodeName == 'li' && bookmark) { 12479 // Start wrapping - if we are in a list node and have a bookmark, then 12480 // we will always begin by wrapping in a new element. 12481 currentWrapElm = applyStyleToList(node, bookmark, wrapElm, newWrappers, process); 12482 } else { 12483 // Start a new wrapper for possible children 12484 currentWrapElm = 0; 12485 12486 each(grep(node.childNodes), process); 12487 12488 if (hasContentEditableState) { 12489 contentEditable = lastContentEditable; // Restore last contentEditable state from stack 12490 } 12491 12492 // End the last wrapper 12493 currentWrapElm = 0; 12494 } 12495 } 12496 12497 // Process siblings from range 12498 each(nodes, process); 12499 }); 12500 12501 // Wrap links inside as well, for example color inside a link when the wrapper is around the link 12502 if (format.wrap_links === false) { 12503 each(newWrappers, function(node) { 12504 function process(node) { 12505 var i, currentWrapElm, children; 12506 12507 if (node.nodeName === 'A') { 12508 currentWrapElm = dom.clone(wrapElm, FALSE); 12509 newWrappers.push(currentWrapElm); 12510 12511 children = grep(node.childNodes); 12512 for (i = 0; i < children.length; i++) { 12513 currentWrapElm.appendChild(children[i]); 12514 } 12515 12516 node.appendChild(currentWrapElm); 12517 } 12518 12519 each(grep(node.childNodes), process); 12520 } 12521 12522 process(node); 12523 }); 12524 } 12525 12526 // Cleanup 12527 each(newWrappers, function(node) { 12528 var childCount; 12529 12530 function getChildCount(node) { 12531 var count = 0; 12532 12533 each(node.childNodes, function(node) { 12534 if (!isWhiteSpaceNode(node) && !isBookmarkNode(node)) { 12535 count++; 12536 } 12537 }); 12538 12539 return count; 12540 } 12541 12542 function mergeStyles(node) { 12543 var child, clone; 12544 12545 each(node.childNodes, function(node) { 12546 if (node.nodeType == 1 && !isBookmarkNode(node) && !isCaretNode(node)) { 12547 child = node; 12548 return FALSE; // break loop 12549 } 12550 }); 12551 12552 // If child was found and of the same type as the current node 12553 if (child && !isBookmarkNode(child) && matchName(child, format)) { 12554 clone = dom.clone(child, FALSE); 12555 setElementFormat(clone); 12556 12557 dom.replace(clone, node, TRUE); 12558 dom.remove(child, 1); 12559 } 12560 12561 return clone || node; 12562 } 12563 12564 childCount = getChildCount(node); 12565 12566 // Remove empty nodes but only if there is multiple wrappers and they are not block 12567 // elements so never remove single <h1></h1> since that would remove the 12568 // currrent empty block element where the caret is at 12569 if ((newWrappers.length > 1 || !isBlock(node)) && childCount === 0) { 12570 dom.remove(node, 1); 12571 return; 12572 } 12573 12574 if (format.inline || format.wrapper) { 12575 // Merges the current node with it's children of similar type to reduce the number of elements 12576 if (!format.exact && childCount === 1) { 12577 node = mergeStyles(node); 12578 } 12579 12580 // Remove/merge children 12581 each(formatList, function(format) { 12582 // Merge all children of similar type will move styles from child to parent 12583 // this: <span style="color:red"><b><span style="color:red; font-size:10px">text</span></b></span> 12584 // will become: <span style="color:red"><b><span style="font-size:10px">text</span></b></span> 12585 each(dom.select(format.inline, node), function(child) { 12586 var parent; 12587 12588 if (isBookmarkNode(child)) { 12589 return; 12590 } 12591 12592 // When wrap_links is set to false we don't want 12593 // to remove the format on children within links 12594 if (format.wrap_links === false) { 12595 parent = child.parentNode; 12596 12597 do { 12598 if (parent.nodeName === 'A') { 12599 return; 12600 } 12601 } while ((parent = parent.parentNode)); 12602 } 12603 12604 removeFormat(format, vars, child, format.exact ? child : null); 12605 }); 12606 }); 12607 12608 // Remove child if direct parent is of same type 12609 if (matchNode(node.parentNode, name, vars)) { 12610 dom.remove(node, 1); 12611 node = 0; 12612 return TRUE; 12613 } 12614 12615 // Look for parent with similar style format 12616 if (format.merge_with_parents) { 12617 dom.getParent(node.parentNode, function(parent) { 12618 if (matchNode(parent, name, vars)) { 12619 dom.remove(node, 1); 12620 node = 0; 12621 return TRUE; 12622 } 12623 }); 12624 } 12625 12626 // Merge next and previous siblings if they are similar <b>text</b><b>text</b> becomes <b>texttext</b> 12627 if (node && format.merge_siblings !== false) { 12628 node = mergeSiblings(getNonWhiteSpaceSibling(node), node); 12629 node = mergeSiblings(node, getNonWhiteSpaceSibling(node, TRUE)); 12630 } 12631 } 12632 }); 12633 } 12634 12635 if (format) { 12636 if (node) { 12637 if (node.nodeType) { 12638 rng = dom.createRng(); 12639 rng.setStartBefore(node); 12640 rng.setEndAfter(node); 12641 applyRngStyle(expandRng(rng, formatList), null, true); 12642 } else { 12643 applyRngStyle(node, null, true); 12644 } 12645 } else { 12646 if (!isCollapsed || !format.inline || dom.select('td.mce-item-selected,th.mce-item-selected').length) { 12647 // Obtain selection node before selection is unselected by applyRngStyle() 12648 var curSelNode = ed.selection.getNode(); 12649 12650 // If the formats have a default block and we can't find a parent block then 12651 // start wrapping it with a DIV this is for forced_root_blocks: false 12652 // It's kind of a hack but people should be using the default block type P since all desktop editors work that way 12653 if (!forcedRootBlock && formatList[0].defaultBlock && !dom.getParent(curSelNode, dom.isBlock)) { 12654 apply(formatList[0].defaultBlock); 12655 } 12656 12657 // Apply formatting to selection 12658 ed.selection.setRng(adjustSelectionToVisibleSelection()); 12659 bookmark = selection.getBookmark(); 12660 applyRngStyle(expandRng(selection.getRng(TRUE), formatList), bookmark); 12661 12662 // Colored nodes should be underlined so that the color of the underline matches the text color. 12663 if (format.styles && (format.styles.color || format.styles.textDecoration)) { 12664 walk(curSelNode, processUnderlineAndColor, 'childNodes'); 12665 processUnderlineAndColor(curSelNode); 12666 } 12667 12668 selection.moveToBookmark(bookmark); 12669 moveStart(selection.getRng(TRUE)); 12670 ed.nodeChanged(); 12671 } else { 12672 performCaretAction('apply', name, vars); 12673 } 12674 } 12675 } 12676 } 12677 12678 /** 12679 * Removes the specified format from the current selection or specified node. 12680 * 12681 * @method remove 12682 * @param {String} name Name of format to remove. 12683 * @param {Object} vars Optional list of variables to replace within format before removing it. 12684 * @param {Node/Range} node Optional node or DOM range to remove the format from defaults to current selection. 12685 */ 12686 function remove(name, vars, node) { 12687 var formatList = get(name), format = formatList[0], bookmark, rng, contentEditable = true; 12688 12689 // Merges the styles for each node 12690 function process(node) { 12691 var children, i, l, lastContentEditable, hasContentEditableState; 12692 12693 // Node has a contentEditable value 12694 if (node.nodeType === 1 && getContentEditable(node)) { 12695 lastContentEditable = contentEditable; 12696 contentEditable = getContentEditable(node) === "true"; 12697 hasContentEditableState = true; // We don't want to wrap the container only it's children 12698 } 12699 12700 // Grab the children first since the nodelist might be changed 12701 children = grep(node.childNodes); 12702 12703 // Process current node 12704 if (contentEditable && !hasContentEditableState) { 12705 for (i = 0, l = formatList.length; i < l; i++) { 12706 if (removeFormat(formatList[i], vars, node, node)) { 12707 break; 12708 } 12709 } 12710 } 12711 12712 // Process the children 12713 if (format.deep) { 12714 if (children.length) { 12715 for (i = 0, l = children.length; i < l; i++) { 12716 process(children[i]); 12717 } 12718 12719 if (hasContentEditableState) { 12720 contentEditable = lastContentEditable; // Restore last contentEditable state from stack 12721 } 12722 } 12723 } 12724 } 12725 12726 function findFormatRoot(container) { 12727 var formatRoot; 12728 12729 // Find format root 12730 each(getParents(container.parentNode).reverse(), function(parent) { 12731 var format; 12732 12733 // Find format root element 12734 if (!formatRoot && parent.id != '_start' && parent.id != '_end') { 12735 // Is the node matching the format we are looking for 12736 format = matchNode(parent, name, vars); 12737 if (format && format.split !== false) { 12738 formatRoot = parent; 12739 } 12740 } 12741 }); 12742 12743 return formatRoot; 12744 } 12745 12746 function wrapAndSplit(format_root, container, target, split) { 12747 var parent, clone, lastClone, firstClone, i, formatRootParent; 12748 12749 // Format root found then clone formats and split it 12750 if (format_root) { 12751 formatRootParent = format_root.parentNode; 12752 12753 for (parent = container.parentNode; parent && parent != formatRootParent; parent = parent.parentNode) { 12754 clone = dom.clone(parent, FALSE); 12755 12756 for (i = 0; i < formatList.length; i++) { 12757 if (removeFormat(formatList[i], vars, clone, clone)) { 12758 clone = 0; 12759 break; 12760 } 12761 } 12762 12763 // Build wrapper node 12764 if (clone) { 12765 if (lastClone) { 12766 clone.appendChild(lastClone); 12767 } 12768 12769 if (!firstClone) { 12770 firstClone = clone; 12771 } 12772 12773 lastClone = clone; 12774 } 12775 } 12776 12777 // Never split block elements if the format is mixed 12778 if (split && (!format.mixed || !isBlock(format_root))) { 12779 container = dom.split(format_root, container); 12780 } 12781 12782 // Wrap container in cloned formats 12783 if (lastClone) { 12784 target.parentNode.insertBefore(lastClone, target); 12785 firstClone.appendChild(target); 12786 } 12787 } 12788 12789 return container; 12790 } 12791 12792 function splitToFormatRoot(container) { 12793 return wrapAndSplit(findFormatRoot(container), container, container, true); 12794 } 12795 12796 function unwrap(start) { 12797 var node = dom.get(start ? '_start' : '_end'), 12798 out = node[start ? 'firstChild' : 'lastChild']; 12799 12800 // If the end is placed within the start the result will be removed 12801 // So this checks if the out node is a bookmark node if it is it 12802 // checks for another more suitable node 12803 if (isBookmarkNode(out)) { 12804 out = out[start ? 'firstChild' : 'lastChild']; 12805 } 12806 12807 dom.remove(node, true); 12808 12809 return out; 12810 } 12811 12812 function removeRngStyle(rng) { 12813 var startContainer, endContainer; 12814 var commonAncestorContainer = rng.commonAncestorContainer; 12815 12816 rng = expandRng(rng, formatList, TRUE); 12817 12818 if (format.split) { 12819 startContainer = getContainer(rng, TRUE); 12820 endContainer = getContainer(rng); 12821 12822 if (startContainer != endContainer) { 12823 // WebKit will render the table incorrectly if we wrap a TH or TD in a SPAN 12824 // so let's see if we can use the first child instead 12825 // This will happen if you triple click a table cell and use remove formatting 12826 if (/^(TR|TH|TD)$/.test(startContainer.nodeName) && startContainer.firstChild) { 12827 if (startContainer.nodeName == "TR") { 12828 startContainer = startContainer.firstChild.firstChild || startContainer; 12829 } else { 12830 startContainer = startContainer.firstChild || startContainer; 12831 } 12832 } 12833 12834 // Try to adjust endContainer as well if cells on the same row were selected - bug #6410 12835 if (commonAncestorContainer && 12836 /^T(HEAD|BODY|FOOT|R)$/.test(commonAncestorContainer.nodeName) && 12837 /^(TH|TD)$/.test(endContainer.nodeName) && endContainer.firstChild) { 12838 endContainer = endContainer.firstChild || endContainer; 12839 } 12840 12841 // Wrap start/end nodes in span element since these might be cloned/moved 12842 startContainer = wrap(startContainer, 'span', {id: '_start', 'data-mce-type': 'bookmark'}); 12843 endContainer = wrap(endContainer, 'span', {id: '_end', 'data-mce-type': 'bookmark'}); 12844 12845 // Split start/end 12846 splitToFormatRoot(startContainer); 12847 splitToFormatRoot(endContainer); 12848 12849 // Unwrap start/end to get real elements again 12850 startContainer = unwrap(TRUE); 12851 endContainer = unwrap(); 12852 } else { 12853 startContainer = endContainer = splitToFormatRoot(startContainer); 12854 } 12855 12856 // Update range positions since they might have changed after the split operations 12857 rng.startContainer = startContainer.parentNode; 12858 rng.startOffset = nodeIndex(startContainer); 12859 rng.endContainer = endContainer.parentNode; 12860 rng.endOffset = nodeIndex(endContainer) + 1; 12861 } 12862 12863 // Remove items between start/end 12864 rangeUtils.walk(rng, function(nodes) { 12865 each(nodes, function(node) { 12866 process(node); 12867 12868 // Remove parent span if it only contains text-decoration: underline, yet a parent node is also underlined. 12869 if (node.nodeType === 1 && ed.dom.getStyle(node, 'text-decoration') === 'underline' && 12870 node.parentNode && getTextDecoration(node.parentNode) === 'underline') { 12871 removeFormat({ 12872 'deep': false, 12873 'exact': true, 12874 'inline': 'span', 12875 'styles': { 12876 'textDecoration': 'underline' 12877 } 12878 }, null, node); 12879 } 12880 }); 12881 }); 12882 } 12883 12884 // Handle node 12885 if (node) { 12886 if (node.nodeType) { 12887 rng = dom.createRng(); 12888 rng.setStartBefore(node); 12889 rng.setEndAfter(node); 12890 removeRngStyle(rng); 12891 } else { 12892 removeRngStyle(node); 12893 } 12894 12895 return; 12896 } 12897 12898 if (!selection.isCollapsed() || !format.inline || dom.select('td.mce-item-selected,th.mce-item-selected').length) { 12899 bookmark = selection.getBookmark(); 12900 removeRngStyle(selection.getRng(TRUE)); 12901 selection.moveToBookmark(bookmark); 12902 12903 // Check if start element still has formatting then we are at: "<b>text|</b>text" 12904 // and need to move the start into the next text node 12905 if (format.inline && match(name, vars, selection.getStart())) { 12906 moveStart(selection.getRng(true)); 12907 } 12908 12909 ed.nodeChanged(); 12910 } else { 12911 performCaretAction('remove', name, vars); 12912 } 12913 } 12914 12915 /** 12916 * Toggles the specified format on/off. 12917 * 12918 * @method toggle 12919 * @param {String} name Name of format to apply/remove. 12920 * @param {Object} vars Optional list of variables to replace within format before applying/removing it. 12921 * @param {Node} node Optional node to apply the format to or remove from. Defaults to current selection. 12922 */ 12923 function toggle(name, vars, node) { 12924 var fmt = get(name); 12925 12926 if (match(name, vars, node) && (!('toggle' in fmt[0]) || fmt[0].toggle)) { 12927 remove(name, vars, node); 12928 } else { 12929 apply(name, vars, node); 12930 } 12931 } 12932 12933 /** 12934 * Return true/false if the specified node has the specified format. 12935 * 12936 * @method matchNode 12937 * @param {Node} node Node to check the format on. 12938 * @param {String} name Format name to check. 12939 * @param {Object} vars Optional list of variables to replace before checking it. 12940 * @param {Boolean} similar Match format that has similar properties. 12941 * @return {Object} Returns the format object it matches or undefined if it doesn't match. 12942 */ 12943 function matchNode(node, name, vars, similar) { 12944 var formatList = get(name), format, i, classes; 12945 12946 function matchItems(node, format, item_name) { 12947 var key, value, items = format[item_name], i; 12948 12949 // Custom match 12950 if (format.onmatch) { 12951 return format.onmatch(node, format, item_name); 12952 } 12953 12954 // Check all items 12955 if (items) { 12956 // Non indexed object 12957 if (items.length === undef) { 12958 for (key in items) { 12959 if (items.hasOwnProperty(key)) { 12960 if (item_name === 'attributes') { 12961 value = dom.getAttrib(node, key); 12962 } else { 12963 value = getStyle(node, key); 12964 } 12965 12966 if (similar && !value && !format.exact) { 12967 return; 12968 } 12969 12970 if ((!similar || format.exact) && !isEq(value, normalizeStyleValue(replaceVars(items[key], vars), key))) { 12971 return; 12972 } 12973 } 12974 } 12975 } else { 12976 // Only one match needed for indexed arrays 12977 for (i = 0; i < items.length; i++) { 12978 if (item_name === 'attributes' ? dom.getAttrib(node, items[i]) : getStyle(node, items[i])) { 12979 return format; 12980 } 12981 } 12982 } 12983 } 12984 12985 return format; 12986 } 12987 12988 if (formatList && node) { 12989 // Check each format in list 12990 for (i = 0; i < formatList.length; i++) { 12991 format = formatList[i]; 12992 12993 // Name name, attributes, styles and classes 12994 if (matchName(node, format) && matchItems(node, format, 'attributes') && matchItems(node, format, 'styles')) { 12995 // Match classes 12996 if ((classes = format.classes)) { 12997 for (i = 0; i < classes.length; i++) { 12998 if (!dom.hasClass(node, classes[i])) { 12999 return; 13000 } 13001 } 13002 } 13003 13004 return format; 13005 } 13006 } 13007 } 13008 } 13009 13010 /** 13011 * Matches the current selection or specified node against the specified format name. 13012 * 13013 * @method match 13014 * @param {String} name Name of format to match. 13015 * @param {Object} vars Optional list of variables to replace before checking it. 13016 * @param {Node} node Optional node to check. 13017 * @return {boolean} true/false if the specified selection/node matches the format. 13018 */ 13019 function match(name, vars, node) { 13020 var startNode; 13021 13022 function matchParents(node) { 13023 var root = dom.getRoot(); 13024 13025 if (node === root) { 13026 return false; 13027 } 13028 13029 // Find first node with similar format settings 13030 node = dom.getParent(node, function(node) { 13031 return node.parentNode === root || !!matchNode(node, name, vars, true); 13032 }); 13033 13034 // Do an exact check on the similar format element 13035 return matchNode(node, name, vars); 13036 } 13037 13038 // Check specified node 13039 if (node) { 13040 return matchParents(node); 13041 } 13042 13043 // Check selected node 13044 node = selection.getNode(); 13045 if (matchParents(node)) { 13046 return TRUE; 13047 } 13048 13049 // Check start node if it's different 13050 startNode = selection.getStart(); 13051 if (startNode != node) { 13052 if (matchParents(startNode)) { 13053 return TRUE; 13054 } 13055 } 13056 13057 return FALSE; 13058 } 13059 13060 /** 13061 * Matches the current selection against the array of formats and returns a new array with matching formats. 13062 * 13063 * @method matchAll 13064 * @param {Array} names Name of format to match. 13065 * @param {Object} vars Optional list of variables to replace before checking it. 13066 * @return {Array} Array with matched formats. 13067 */ 13068 function matchAll(names, vars) { 13069 var startElement, matchedFormatNames = [], checkedMap = {}; 13070 13071 // Check start of selection for formats 13072 startElement = selection.getStart(); 13073 dom.getParent(startElement, function(node) { 13074 var i, name; 13075 13076 for (i = 0; i < names.length; i++) { 13077 name = names[i]; 13078 13079 if (!checkedMap[name] && matchNode(node, name, vars)) { 13080 checkedMap[name] = true; 13081 matchedFormatNames.push(name); 13082 } 13083 } 13084 }, dom.getRoot()); 13085 13086 return matchedFormatNames; 13087 } 13088 13089 /** 13090 * Returns true/false if the specified format can be applied to the current selection or not. It 13091 * will currently only check the state for selector formats, it returns true on all other format types. 13092 * 13093 * @method canApply 13094 * @param {String} name Name of format to check. 13095 * @return {boolean} true/false if the specified format can be applied to the current selection/node. 13096 */ 13097 function canApply(name) { 13098 var formatList = get(name), startNode, parents, i, x, selector; 13099 13100 if (formatList) { 13101 startNode = selection.getStart(); 13102 parents = getParents(startNode); 13103 13104 for (x = formatList.length - 1; x >= 0; x--) { 13105 selector = formatList[x].selector; 13106 13107 // Format is not selector based then always return TRUE 13108 // Is it has a defaultBlock then it's likely it can be applied for example align on a non block element line 13109 if (!selector || formatList[x].defaultBlock) { 13110 return TRUE; 13111 } 13112 13113 for (i = parents.length - 1; i >= 0; i--) { 13114 if (dom.is(parents[i], selector)) { 13115 return TRUE; 13116 } 13117 } 13118 } 13119 } 13120 13121 return FALSE; 13122 } 13123 13124 /** 13125 * Executes the specified callback when the current selection matches the formats or not. 13126 * 13127 * @method formatChanged 13128 * @param {String} formats Comma separated list of formats to check for. 13129 * @param {function} callback Callback with state and args when the format is changed/toggled on/off. 13130 * @param {Boolean} similar True/false state if the match should handle similar or exact formats. 13131 */ 13132 function formatChanged(formats, callback, similar) { 13133 var currentFormats; 13134 13135 // Setup format node change logic 13136 if (!formatChangeData) { 13137 formatChangeData = {}; 13138 currentFormats = {}; 13139 13140 ed.on('NodeChange', function(e) { 13141 var parents = getParents(e.element), matchedFormats = {}; 13142 13143 // Check for new formats 13144 each(formatChangeData, function(callbacks, format) { 13145 each(parents, function(node) { 13146 if (matchNode(node, format, {}, callbacks.similar)) { 13147 if (!currentFormats[format]) { 13148 // Execute callbacks 13149 each(callbacks, function(callback) { 13150 callback(true, {node: node, format: format, parents: parents}); 13151 }); 13152 13153 currentFormats[format] = callbacks; 13154 } 13155 13156 matchedFormats[format] = callbacks; 13157 return false; 13158 } 13159 }); 13160 }); 13161 13162 // Check if current formats still match 13163 each(currentFormats, function(callbacks, format) { 13164 if (!matchedFormats[format]) { 13165 delete currentFormats[format]; 13166 13167 each(callbacks, function(callback) { 13168 callback(false, {node: e.element, format: format, parents: parents}); 13169 }); 13170 } 13171 }); 13172 }); 13173 } 13174 13175 // Add format listeners 13176 each(formats.split(','), function(format) { 13177 if (!formatChangeData[format]) { 13178 formatChangeData[format] = []; 13179 formatChangeData[format].similar = similar; 13180 } 13181 13182 formatChangeData[format].push(callback); 13183 }); 13184 13185 return this; 13186 } 13187 13188 /** 13189 * Returns a preview css text for the specified format. 13190 * 13191 * @method getCssText 13192 * @param {String/Object} format Format to generate preview css text for. 13193 * @return {String} Css text for the specified format. 13194 * @example 13195 * var cssText1 = editor.formatter.getCssText('bold'); 13196 * var cssText2 = editor.formatter.getCssText({inline: 'b'}); 13197 */ 13198 function getCssText(format) { 13199 return Preview.getCssText(ed, format); 13200 } 13201 13202 // Expose to public 13203 extend(this, { 13204 get: get, 13205 register: register, 13206 apply: apply, 13207 remove: remove, 13208 toggle: toggle, 13209 match: match, 13210 matchAll: matchAll, 13211 matchNode: matchNode, 13212 canApply: canApply, 13213 formatChanged: formatChanged, 13214 getCssText: getCssText 13215 }); 13216 13217 // Initialize 13218 defaultFormats(); 13219 addKeyboardShortcuts(); 13220 ed.on('BeforeGetContent', function() { 13221 if (markCaretContainersBogus) { 13222 markCaretContainersBogus(); 13223 } 13224 }); 13225 ed.on('mouseup keydown', function(e) { 13226 if (disableCaretContainer) { 13227 disableCaretContainer(e); 13228 } 13229 }); 13230 13231 // Private functions 13232 13233 /** 13234 * Checks if the specified nodes name matches the format inline/block or selector. 13235 * 13236 * @private 13237 * @param {Node} node Node to match against the specified format. 13238 * @param {Object} format Format object o match with. 13239 * @return {boolean} true/false if the format matches. 13240 */ 13241 function matchName(node, format) { 13242 // Check for inline match 13243 if (isEq(node, format.inline)) { 13244 return TRUE; 13245 } 13246 13247 // Check for block match 13248 if (isEq(node, format.block)) { 13249 return TRUE; 13250 } 13251 13252 // Check for selector match 13253 if (format.selector) { 13254 return node.nodeType == 1 && dom.is(node, format.selector); 13255 } 13256 } 13257 13258 /** 13259 * Compares two string/nodes regardless of their case. 13260 * 13261 * @private 13262 * @param {String/Node} Node or string to compare. 13263 * @param {String/Node} Node or string to compare. 13264 * @return {boolean} True/false if they match. 13265 */ 13266 function isEq(str1, str2) { 13267 str1 = str1 || ''; 13268 str2 = str2 || ''; 13269 13270 str1 = '' + (str1.nodeName || str1); 13271 str2 = '' + (str2.nodeName || str2); 13272 13273 return str1.toLowerCase() == str2.toLowerCase(); 13274 } 13275 13276 /** 13277 * Returns the style by name on the specified node. This method modifies the style 13278 * contents to make it more easy to match. This will resolve a few browser issues. 13279 * 13280 * @private 13281 * @param {Node} node to get style from. 13282 * @param {String} name Style name to get. 13283 * @return {String} Style item value. 13284 */ 13285 function getStyle(node, name) { 13286 return normalizeStyleValue(dom.getStyle(node, name), name); 13287 } 13288 13289 /** 13290 * Normalize style value by name. This method modifies the style contents 13291 * to make it more easy to match. This will resolve a few browser issues. 13292 * 13293 * @private 13294 * @param {Node} node to get style from. 13295 * @param {String} name Style name to get. 13296 * @return {String} Style item value. 13297 */ 13298 function normalizeStyleValue(value, name) { 13299 // Force the format to hex 13300 if (name == 'color' || name == 'backgroundColor') { 13301 value = dom.toHex(value); 13302 } 13303 13304 // Opera will return bold as 700 13305 if (name == 'fontWeight' && value == 700) { 13306 value = 'bold'; 13307 } 13308 13309 // Normalize fontFamily so "'Font name', Font" becomes: "Font name,Font" 13310 if (name == 'fontFamily') { 13311 value = value.replace(/[\'\"]/g, '').replace(/,\s+/g, ','); 13312 } 13313 13314 return '' + value; 13315 } 13316 13317 /** 13318 * Replaces variables in the value. The variable format is %var. 13319 * 13320 * @private 13321 * @param {String} value Value to replace variables in. 13322 * @param {Object} vars Name/value array with variables to replace. 13323 * @return {String} New value with replaced variables. 13324 */ 13325 function replaceVars(value, vars) { 13326 if (typeof(value) != "string") { 13327 value = value(vars); 13328 } else if (vars) { 13329 value = value.replace(/%(\w+)/g, function(str, name) { 13330 return vars[name] || str; 13331 }); 13332 } 13333 13334 return value; 13335 } 13336 13337 function isWhiteSpaceNode(node) { 13338 return node && node.nodeType === 3 && /^([\t \r\n]+|)$/.test(node.nodeValue); 13339 } 13340 13341 function wrap(node, name, attrs) { 13342 var wrapper = dom.create(name, attrs); 13343 13344 node.parentNode.insertBefore(wrapper, node); 13345 wrapper.appendChild(node); 13346 13347 return wrapper; 13348 } 13349 13350 /** 13351 * Expands the specified range like object to depending on format. 13352 * 13353 * For example on block formats it will move the start/end position 13354 * to the beginning of the current block. 13355 * 13356 * @private 13357 * @param {Object} rng Range like object. 13358 * @param {Array} formats Array with formats to expand by. 13359 * @return {Object} Expanded range like object. 13360 */ 13361 function expandRng(rng, format, remove) { 13362 var lastIdx, leaf, endPoint, 13363 startContainer = rng.startContainer, 13364 startOffset = rng.startOffset, 13365 endContainer = rng.endContainer, 13366 endOffset = rng.endOffset; 13367 13368 // This function walks up the tree if there is no siblings before/after the node 13369 function findParentContainer(start) { 13370 var container, parent, sibling, siblingName, root; 13371 13372 container = parent = start ? startContainer : endContainer; 13373 siblingName = start ? 'previousSibling' : 'nextSibling'; 13374 root = dom.getRoot(); 13375 13376 function isBogusBr(node) { 13377 return node.nodeName == "BR" && node.getAttribute('data-mce-bogus') && !node.nextSibling; 13378 } 13379 13380 // If it's a text node and the offset is inside the text 13381 if (container.nodeType == 3 && !isWhiteSpaceNode(container)) { 13382 if (start ? startOffset > 0 : endOffset < container.nodeValue.length) { 13383 return container; 13384 } 13385 } 13386 13387 /*eslint no-constant-condition:0 */ 13388 while (true) { 13389 // Stop expanding on block elements 13390 if (!format[0].block_expand && isBlock(parent)) { 13391 return parent; 13392 } 13393 13394 // Walk left/right 13395 for (sibling = parent[siblingName]; sibling; sibling = sibling[siblingName]) { 13396 if (!isBookmarkNode(sibling) && !isWhiteSpaceNode(sibling) && !isBogusBr(sibling)) { 13397 return parent; 13398 } 13399 } 13400 13401 // Check if we can move up are we at root level or body level 13402 if (parent.parentNode == root) { 13403 container = parent; 13404 break; 13405 } 13406 13407 parent = parent.parentNode; 13408 } 13409 13410 return container; 13411 } 13412 13413 // This function walks down the tree to find the leaf at the selection. 13414 // The offset is also returned as if node initially a leaf, the offset may be in the middle of the text node. 13415 function findLeaf(node, offset) { 13416 if (offset === undef) { 13417 offset = node.nodeType === 3 ? node.length : node.childNodes.length; 13418 } 13419 13420 while (node && node.hasChildNodes()) { 13421 node = node.childNodes[offset]; 13422 if (node) { 13423 offset = node.nodeType === 3 ? node.length : node.childNodes.length; 13424 } 13425 } 13426 return { node: node, offset: offset }; 13427 } 13428 13429 // If index based start position then resolve it 13430 if (startContainer.nodeType == 1 && startContainer.hasChildNodes()) { 13431 lastIdx = startContainer.childNodes.length - 1; 13432 startContainer = startContainer.childNodes[startOffset > lastIdx ? lastIdx : startOffset]; 13433 13434 if (startContainer.nodeType == 3) { 13435 startOffset = 0; 13436 } 13437 } 13438 13439 // If index based end position then resolve it 13440 if (endContainer.nodeType == 1 && endContainer.hasChildNodes()) { 13441 lastIdx = endContainer.childNodes.length - 1; 13442 endContainer = endContainer.childNodes[endOffset > lastIdx ? lastIdx : endOffset - 1]; 13443 13444 if (endContainer.nodeType == 3) { 13445 endOffset = endContainer.nodeValue.length; 13446 } 13447 } 13448 13449 // Expands the node to the closes contentEditable false element if it exists 13450 function findParentContentEditable(node) { 13451 var parent = node; 13452 13453 while (parent) { 13454 if (parent.nodeType === 1 && getContentEditable(parent)) { 13455 return getContentEditable(parent) === "false" ? parent : node; 13456 } 13457 13458 parent = parent.parentNode; 13459 } 13460 13461 return node; 13462 } 13463 13464 function findWordEndPoint(container, offset, start) { 13465 var walker, node, pos, lastTextNode; 13466 13467 function findSpace(node, offset) { 13468 var pos, pos2, str = node.nodeValue; 13469 13470 if (typeof(offset) == "undefined") { 13471 offset = start ? str.length : 0; 13472 } 13473 13474 if (start) { 13475 pos = str.lastIndexOf(' ', offset); 13476 pos2 = str.lastIndexOf('\u00a0', offset); 13477 pos = pos > pos2 ? pos : pos2; 13478 13479 // Include the space on remove to avoid tag soup 13480 if (pos !== -1 && !remove) { 13481 pos++; 13482 } 13483 } else { 13484 pos = str.indexOf(' ', offset); 13485 pos2 = str.indexOf('\u00a0', offset); 13486 pos = pos !== -1 && (pos2 === -1 || pos < pos2) ? pos : pos2; 13487 } 13488 13489 return pos; 13490 } 13491 13492 if (container.nodeType === 3) { 13493 pos = findSpace(container, offset); 13494 13495 if (pos !== -1) { 13496 return {container: container, offset: pos}; 13497 } 13498 13499 lastTextNode = container; 13500 } 13501 13502 // Walk the nodes inside the block 13503 walker = new TreeWalker(container, dom.getParent(container, isBlock) || ed.getBody()); 13504 while ((node = walker[start ? 'prev' : 'next']())) { 13505 if (node.nodeType === 3) { 13506 lastTextNode = node; 13507 pos = findSpace(node); 13508 13509 if (pos !== -1) { 13510 return {container: node, offset: pos}; 13511 } 13512 } else if (isBlock(node)) { 13513 break; 13514 } 13515 } 13516 13517 if (lastTextNode) { 13518 if (start) { 13519 offset = 0; 13520 } else { 13521 offset = lastTextNode.length; 13522 } 13523 13524 return {container: lastTextNode, offset: offset}; 13525 } 13526 } 13527 13528 function findSelectorEndPoint(container, sibling_name) { 13529 var parents, i, y, curFormat; 13530 13531 if (container.nodeType == 3 && container.nodeValue.length === 0 && container[sibling_name]) { 13532 container = container[sibling_name]; 13533 } 13534 13535 parents = getParents(container); 13536 for (i = 0; i < parents.length; i++) { 13537 for (y = 0; y < format.length; y++) { 13538 curFormat = format[y]; 13539 13540 // If collapsed state is set then skip formats that doesn't match that 13541 if ("collapsed" in curFormat && curFormat.collapsed !== rng.collapsed) { 13542 continue; 13543 } 13544 13545 if (dom.is(parents[i], curFormat.selector)) { 13546 return parents[i]; 13547 } 13548 } 13549 } 13550 13551 return container; 13552 } 13553 13554 function findBlockEndPoint(container, sibling_name) { 13555 var node, root = dom.getRoot(); 13556 13557 // Expand to block of similar type 13558 if (!format[0].wrapper) { 13559 node = dom.getParent(container, format[0].block, root); 13560 } 13561 13562 // Expand to first wrappable block element or any block element 13563 if (!node) { 13564 node = dom.getParent(container.nodeType == 3 ? container.parentNode : container, function(node) { 13565 // Fixes #6183 where it would expand to editable parent element in inline mode 13566 return node != root && isTextBlock(node); 13567 }); 13568 } 13569 13570 // Exclude inner lists from wrapping 13571 if (node && format[0].wrapper) { 13572 node = getParents(node, 'ul,ol').reverse()[0] || node; 13573 } 13574 13575 // Didn't find a block element look for first/last wrappable element 13576 if (!node) { 13577 node = container; 13578 13579 while (node[sibling_name] && !isBlock(node[sibling_name])) { 13580 node = node[sibling_name]; 13581 13582 // Break on BR but include it will be removed later on 13583 // we can't remove it now since we need to check if it can be wrapped 13584 if (isEq(node, 'br')) { 13585 break; 13586 } 13587 } 13588 } 13589 13590 return node || container; 13591 } 13592 13593 // Expand to closest contentEditable element 13594 startContainer = findParentContentEditable(startContainer); 13595 endContainer = findParentContentEditable(endContainer); 13596 13597 // Exclude bookmark nodes if possible 13598 if (isBookmarkNode(startContainer.parentNode) || isBookmarkNode(startContainer)) { 13599 startContainer = isBookmarkNode(startContainer) ? startContainer : startContainer.parentNode; 13600 startContainer = startContainer.nextSibling || startContainer; 13601 13602 if (startContainer.nodeType == 3) { 13603 startOffset = 0; 13604 } 13605 } 13606 13607 if (isBookmarkNode(endContainer.parentNode) || isBookmarkNode(endContainer)) { 13608 endContainer = isBookmarkNode(endContainer) ? endContainer : endContainer.parentNode; 13609 endContainer = endContainer.previousSibling || endContainer; 13610 13611 if (endContainer.nodeType == 3) { 13612 endOffset = endContainer.length; 13613 } 13614 } 13615 13616 if (format[0].inline) { 13617 if (rng.collapsed) { 13618 // Expand left to closest word boundary 13619 endPoint = findWordEndPoint(startContainer, startOffset, true); 13620 if (endPoint) { 13621 startContainer = endPoint.container; 13622 startOffset = endPoint.offset; 13623 } 13624 13625 // Expand right to closest word boundary 13626 endPoint = findWordEndPoint(endContainer, endOffset); 13627 if (endPoint) { 13628 endContainer = endPoint.container; 13629 endOffset = endPoint.offset; 13630 } 13631 } 13632 13633 // Avoid applying formatting to a trailing space. 13634 leaf = findLeaf(endContainer, endOffset); 13635 if (leaf.node) { 13636 while (leaf.node && leaf.offset === 0 && leaf.node.previousSibling) { 13637 leaf = findLeaf(leaf.node.previousSibling); 13638 } 13639 13640 if (leaf.node && leaf.offset > 0 && leaf.node.nodeType === 3 && 13641 leaf.node.nodeValue.charAt(leaf.offset - 1) === ' ') { 13642 13643 if (leaf.offset > 1) { 13644 endContainer = leaf.node; 13645 endContainer.splitText(leaf.offset - 1); 13646 } 13647 } 13648 } 13649 } 13650 13651 // Move start/end point up the tree if the leaves are sharp and if we are in different containers 13652 // Example * becomes !: !<p><b><i>*text</i><i>text*</i></b></p>! 13653 // This will reduce the number of wrapper elements that needs to be created 13654 // Move start point up the tree 13655 if (format[0].inline || format[0].block_expand) { 13656 if (!format[0].inline || (startContainer.nodeType != 3 || startOffset === 0)) { 13657 startContainer = findParentContainer(true); 13658 } 13659 13660 if (!format[0].inline || (endContainer.nodeType != 3 || endOffset === endContainer.nodeValue.length)) { 13661 endContainer = findParentContainer(); 13662 } 13663 } 13664 13665 // Expand start/end container to matching selector 13666 if (format[0].selector && format[0].expand !== FALSE && !format[0].inline) { 13667 // Find new startContainer/endContainer if there is better one 13668 startContainer = findSelectorEndPoint(startContainer, 'previousSibling'); 13669 endContainer = findSelectorEndPoint(endContainer, 'nextSibling'); 13670 } 13671 13672 // Expand start/end container to matching block element or text node 13673 if (format[0].block || format[0].selector) { 13674 // Find new startContainer/endContainer if there is better one 13675 startContainer = findBlockEndPoint(startContainer, 'previousSibling'); 13676 endContainer = findBlockEndPoint(endContainer, 'nextSibling'); 13677 13678 // Non block element then try to expand up the leaf 13679 if (format[0].block) { 13680 if (!isBlock(startContainer)) { 13681 startContainer = findParentContainer(true); 13682 } 13683 13684 if (!isBlock(endContainer)) { 13685 endContainer = findParentContainer(); 13686 } 13687 } 13688 } 13689 13690 // Setup index for startContainer 13691 if (startContainer.nodeType == 1) { 13692 startOffset = nodeIndex(startContainer); 13693 startContainer = startContainer.parentNode; 13694 } 13695 13696 // Setup index for endContainer 13697 if (endContainer.nodeType == 1) { 13698 endOffset = nodeIndex(endContainer) + 1; 13699 endContainer = endContainer.parentNode; 13700 } 13701 13702 // Return new range like object 13703 return { 13704 startContainer: startContainer, 13705 startOffset: startOffset, 13706 endContainer: endContainer, 13707 endOffset: endOffset 13708 }; 13709 } 13710 13711 /** 13712 * Removes the specified format for the specified node. It will also remove the node if it doesn't have 13713 * any attributes if the format specifies it to do so. 13714 * 13715 * @private 13716 * @param {Object} format Format object with items to remove from node. 13717 * @param {Object} vars Name/value object with variables to apply to format. 13718 * @param {Node} node Node to remove the format styles on. 13719 * @param {Node} compare_node Optional compare node, if specified the styles will be compared to that node. 13720 * @return {Boolean} True/false if the node was removed or not. 13721 */ 13722 function removeFormat(format, vars, node, compare_node) { 13723 var i, attrs, stylesModified; 13724 13725 // Check if node matches format 13726 if (!matchName(node, format)) { 13727 return FALSE; 13728 } 13729 13730 // Should we compare with format attribs and styles 13731 if (format.remove != 'all') { 13732 // Remove styles 13733 each(format.styles, function(value, name) { 13734 value = normalizeStyleValue(replaceVars(value, vars), name); 13735 13736 // Indexed array 13737 if (typeof(name) === 'number') { 13738 name = value; 13739 compare_node = 0; 13740 } 13741 13742 if (!compare_node || isEq(getStyle(compare_node, name), value)) { 13743 dom.setStyle(node, name, ''); 13744 } 13745 13746 stylesModified = 1; 13747 }); 13748 13749 // Remove style attribute if it's empty 13750 if (stylesModified && dom.getAttrib(node, 'style') === '') { 13751 node.removeAttribute('style'); 13752 node.removeAttribute('data-mce-style'); 13753 } 13754 13755 // Remove attributes 13756 each(format.attributes, function(value, name) { 13757 var valueOut; 13758 13759 value = replaceVars(value, vars); 13760 13761 // Indexed array 13762 if (typeof(name) === 'number') { 13763 name = value; 13764 compare_node = 0; 13765 } 13766 13767 if (!compare_node || isEq(dom.getAttrib(compare_node, name), value)) { 13768 // Keep internal classes 13769 if (name == 'class') { 13770 value = dom.getAttrib(node, name); 13771 if (value) { 13772 // Build new class value where everything is removed except the internal prefixed classes 13773 valueOut = ''; 13774 each(value.split(/\s+/), function(cls) { 13775 if (/mce\w+/.test(cls)) { 13776 valueOut += (valueOut ? ' ' : '') + cls; 13777 } 13778 }); 13779 13780 // We got some internal classes left 13781 if (valueOut) { 13782 dom.setAttrib(node, name, valueOut); 13783 return; 13784 } 13785 } 13786 } 13787 13788 // IE6 has a bug where the attribute doesn't get removed correctly 13789 if (name == "class") { 13790 node.removeAttribute('className'); 13791 } 13792 13793 // Remove mce prefixed attributes 13794 if (MCE_ATTR_RE.test(name)) { 13795 node.removeAttribute('data-mce-' + name); 13796 } 13797 13798 node.removeAttribute(name); 13799 } 13800 }); 13801 13802 // Remove classes 13803 each(format.classes, function(value) { 13804 value = replaceVars(value, vars); 13805 13806 if (!compare_node || dom.hasClass(compare_node, value)) { 13807 dom.removeClass(node, value); 13808 } 13809 }); 13810 13811 // Check for non internal attributes 13812 attrs = dom.getAttribs(node); 13813 for (i = 0; i < attrs.length; i++) { 13814 if (attrs[i].nodeName.indexOf('_') !== 0) { 13815 return FALSE; 13816 } 13817 } 13818 } 13819 13820 // Remove the inline child if it's empty for example <b> or <span> 13821 if (format.remove != 'none') { 13822 removeNode(node, format); 13823 return TRUE; 13824 } 13825 } 13826 13827 /** 13828 * Removes the node and wrap it's children in paragraphs before doing so or 13829 * appends BR elements to the beginning/end of the block element if forcedRootBlocks is disabled. 13830 * 13831 * If the div in the node below gets removed: 13832 * text<div>text</div>text 13833 * 13834 * Output becomes: 13835 * text<div><br />text<br /></div>text 13836 * 13837 * So when the div is removed the result is: 13838 * text<br />text<br />text 13839 * 13840 * @private 13841 * @param {Node} node Node to remove + apply BR/P elements to. 13842 * @param {Object} format Format rule. 13843 * @return {Node} Input node. 13844 */ 13845 function removeNode(node, format) { 13846 var parentNode = node.parentNode, rootBlockElm; 13847 13848 function find(node, next, inc) { 13849 node = getNonWhiteSpaceSibling(node, next, inc); 13850 13851 return !node || (node.nodeName == 'BR' || isBlock(node)); 13852 } 13853 13854 if (format.block) { 13855 if (!forcedRootBlock) { 13856 // Append BR elements if needed before we remove the block 13857 if (isBlock(node) && !isBlock(parentNode)) { 13858 if (!find(node, FALSE) && !find(node.firstChild, TRUE, 1)) { 13859 node.insertBefore(dom.create('br'), node.firstChild); 13860 } 13861 13862 if (!find(node, TRUE) && !find(node.lastChild, FALSE, 1)) { 13863 node.appendChild(dom.create('br')); 13864 } 13865 } 13866 } else { 13867 // Wrap the block in a forcedRootBlock if we are at the root of document 13868 if (parentNode == dom.getRoot()) { 13869 if (!format.list_block || !isEq(node, format.list_block)) { 13870 each(grep(node.childNodes), function(node) { 13871 if (isValid(forcedRootBlock, node.nodeName.toLowerCase())) { 13872 if (!rootBlockElm) { 13873 rootBlockElm = wrap(node, forcedRootBlock); 13874 dom.setAttribs(rootBlockElm, ed.settings.forced_root_block_attrs); 13875 } else { 13876 rootBlockElm.appendChild(node); 13877 } 13878 } else { 13879 rootBlockElm = 0; 13880 } 13881 }); 13882 } 13883 } 13884 } 13885 } 13886 13887 // Never remove nodes that isn't the specified inline element if a selector is specified too 13888 if (format.selector && format.inline && !isEq(format.inline, node)) { 13889 return; 13890 } 13891 13892 dom.remove(node, 1); 13893 } 13894 13895 /** 13896 * Returns the next/previous non whitespace node. 13897 * 13898 * @private 13899 * @param {Node} node Node to start at. 13900 * @param {boolean} next (Optional) Include next or previous node defaults to previous. 13901 * @param {boolean} inc (Optional) Include the current node in checking. Defaults to false. 13902 * @return {Node} Next or previous node or undefined if it wasn't found. 13903 */ 13904 function getNonWhiteSpaceSibling(node, next, inc) { 13905 if (node) { 13906 next = next ? 'nextSibling' : 'previousSibling'; 13907 13908 for (node = inc ? node : node[next]; node; node = node[next]) { 13909 if (node.nodeType == 1 || !isWhiteSpaceNode(node)) { 13910 return node; 13911 } 13912 } 13913 } 13914 } 13915 13916 /** 13917 * Checks if the specified node is a bookmark node or not. 13918 * 13919 * @private 13920 * @param {Node} node Node to check if it's a bookmark node or not. 13921 * @return {Boolean} true/false if the node is a bookmark node. 13922 */ 13923 function isBookmarkNode(node) { 13924 return node && node.nodeType == 1 && node.getAttribute('data-mce-type') == 'bookmark'; 13925 } 13926 13927 /** 13928 * Merges the next/previous sibling element if they match. 13929 * 13930 * @private 13931 * @param {Node} prev Previous node to compare/merge. 13932 * @param {Node} next Next node to compare/merge. 13933 * @return {Node} Next node if we didn't merge and prev node if we did. 13934 */ 13935 function mergeSiblings(prev, next) { 13936 var sibling, tmpSibling; 13937 13938 /** 13939 * Compares two nodes and checks if it's attributes and styles matches. 13940 * This doesn't compare classes as items since their order is significant. 13941 * 13942 * @private 13943 * @param {Node} node1 First node to compare with. 13944 * @param {Node} node2 Second node to compare with. 13945 * @return {boolean} True/false if the nodes are the same or not. 13946 */ 13947 function compareElements(node1, node2) { 13948 // Not the same name 13949 if (node1.nodeName != node2.nodeName) { 13950 return FALSE; 13951 } 13952 13953 /** 13954 * Returns all the nodes attributes excluding internal ones, styles and classes. 13955 * 13956 * @private 13957 * @param {Node} node Node to get attributes from. 13958 * @return {Object} Name/value object with attributes and attribute values. 13959 */ 13960 function getAttribs(node) { 13961 var attribs = {}; 13962 13963 each(dom.getAttribs(node), function(attr) { 13964 var name = attr.nodeName.toLowerCase(); 13965 13966 // Don't compare internal attributes or style 13967 if (name.indexOf('_') !== 0 && name !== 'style' && name !== 'data-mce-style') { 13968 attribs[name] = dom.getAttrib(node, name); 13969 } 13970 }); 13971 13972 return attribs; 13973 } 13974 13975 /** 13976 * Compares two objects checks if it's key + value exists in the other one. 13977 * 13978 * @private 13979 * @param {Object} obj1 First object to compare. 13980 * @param {Object} obj2 Second object to compare. 13981 * @return {boolean} True/false if the objects matches or not. 13982 */ 13983 function compareObjects(obj1, obj2) { 13984 var value, name; 13985 13986 for (name in obj1) { 13987 // Obj1 has item obj2 doesn't have 13988 if (obj1.hasOwnProperty(name)) { 13989 value = obj2[name]; 13990 13991 // Obj2 doesn't have obj1 item 13992 if (value === undef) { 13993 return FALSE; 13994 } 13995 13996 // Obj2 item has a different value 13997 if (obj1[name] != value) { 13998 return FALSE; 13999 } 14000 14001 // Delete similar value 14002 delete obj2[name]; 14003 } 14004 } 14005 14006 // Check if obj 2 has something obj 1 doesn't have 14007 for (name in obj2) { 14008 // Obj2 has item obj1 doesn't have 14009 if (obj2.hasOwnProperty(name)) { 14010 return FALSE; 14011 } 14012 } 14013 14014 return TRUE; 14015 } 14016 14017 // Attribs are not the same 14018 if (!compareObjects(getAttribs(node1), getAttribs(node2))) { 14019 return FALSE; 14020 } 14021 14022 // Styles are not the same 14023 if (!compareObjects(dom.parseStyle(dom.getAttrib(node1, 'style')), dom.parseStyle(dom.getAttrib(node2, 'style')))) { 14024 return FALSE; 14025 } 14026 14027 return !isBookmarkNode(node1) && !isBookmarkNode(node2); 14028 } 14029 14030 function findElementSibling(node, sibling_name) { 14031 for (sibling = node; sibling; sibling = sibling[sibling_name]) { 14032 if (sibling.nodeType == 3 && sibling.nodeValue.length !== 0) { 14033 return node; 14034 } 14035 14036 if (sibling.nodeType == 1 && !isBookmarkNode(sibling)) { 14037 return sibling; 14038 } 14039 } 14040 14041 return node; 14042 } 14043 14044 // Check if next/prev exists and that they are elements 14045 if (prev && next) { 14046 // If previous sibling is empty then jump over it 14047 prev = findElementSibling(prev, 'previousSibling'); 14048 next = findElementSibling(next, 'nextSibling'); 14049 14050 // Compare next and previous nodes 14051 if (compareElements(prev, next)) { 14052 // Append nodes between 14053 for (sibling = prev.nextSibling; sibling && sibling != next;) { 14054 tmpSibling = sibling; 14055 sibling = sibling.nextSibling; 14056 prev.appendChild(tmpSibling); 14057 } 14058 14059 // Remove next node 14060 dom.remove(next); 14061 14062 // Move children into prev node 14063 each(grep(next.childNodes), function(node) { 14064 prev.appendChild(node); 14065 }); 14066 14067 return prev; 14068 } 14069 } 14070 14071 return next; 14072 } 14073 14074 function getContainer(rng, start) { 14075 var container, offset, lastIdx; 14076 14077 container = rng[start ? 'startContainer' : 'endContainer']; 14078 offset = rng[start ? 'startOffset' : 'endOffset']; 14079 14080 if (container.nodeType == 1) { 14081 lastIdx = container.childNodes.length - 1; 14082 14083 if (!start && offset) { 14084 offset--; 14085 } 14086 14087 container = container.childNodes[offset > lastIdx ? lastIdx : offset]; 14088 } 14089 14090 // If start text node is excluded then walk to the next node 14091 if (container.nodeType === 3 && start && offset >= container.nodeValue.length) { 14092 container = new TreeWalker(container, ed.getBody()).next() || container; 14093 } 14094 14095 // If end text node is excluded then walk to the previous node 14096 if (container.nodeType === 3 && !start && offset === 0) { 14097 container = new TreeWalker(container, ed.getBody()).prev() || container; 14098 } 14099 14100 return container; 14101 } 14102 14103 function performCaretAction(type, name, vars) { 14104 var caretContainerId = '_mce_caret', debug = ed.settings.caret_debug; 14105 14106 // Creates a caret container bogus element 14107 function createCaretContainer(fill) { 14108 var caretContainer = dom.create('span', {id: caretContainerId, 'data-mce-bogus': true, style: debug ? 'color:red' : ''}); 14109 14110 if (fill) { 14111 caretContainer.appendChild(ed.getDoc().createTextNode(INVISIBLE_CHAR)); 14112 } 14113 14114 return caretContainer; 14115 } 14116 14117 function isCaretContainerEmpty(node, nodes) { 14118 while (node) { 14119 if ((node.nodeType === 3 && node.nodeValue !== INVISIBLE_CHAR) || node.childNodes.length > 1) { 14120 return false; 14121 } 14122 14123 // Collect nodes 14124 if (nodes && node.nodeType === 1) { 14125 nodes.push(node); 14126 } 14127 14128 node = node.firstChild; 14129 } 14130 14131 return true; 14132 } 14133 14134 // Returns any parent caret container element 14135 function getParentCaretContainer(node) { 14136 while (node) { 14137 if (node.id === caretContainerId) { 14138 return node; 14139 } 14140 14141 node = node.parentNode; 14142 } 14143 } 14144 14145 // Finds the first text node in the specified node 14146 function findFirstTextNode(node) { 14147 var walker; 14148 14149 if (node) { 14150 walker = new TreeWalker(node, node); 14151 14152 for (node = walker.current(); node; node = walker.next()) { 14153 if (node.nodeType === 3) { 14154 return node; 14155 } 14156 } 14157 } 14158 } 14159 14160 // Removes the caret container for the specified node or all on the current document 14161 function removeCaretContainer(node, move_caret) { 14162 var child, rng; 14163 14164 if (!node) { 14165 node = getParentCaretContainer(selection.getStart()); 14166 14167 if (!node) { 14168 while ((node = dom.get(caretContainerId))) { 14169 removeCaretContainer(node, false); 14170 } 14171 } 14172 } else { 14173 rng = selection.getRng(true); 14174 14175 if (isCaretContainerEmpty(node)) { 14176 if (move_caret !== false) { 14177 rng.setStartBefore(node); 14178 rng.setEndBefore(node); 14179 } 14180 14181 dom.remove(node); 14182 } else { 14183 child = findFirstTextNode(node); 14184 14185 if (child.nodeValue.charAt(0) === INVISIBLE_CHAR) { 14186 child = child.deleteData(0, 1); 14187 } 14188 14189 dom.remove(node, 1); 14190 } 14191 14192 selection.setRng(rng); 14193 } 14194 } 14195 14196 // Applies formatting to the caret postion 14197 function applyCaretFormat() { 14198 var rng, caretContainer, textNode, offset, bookmark, container, text; 14199 14200 rng = selection.getRng(true); 14201 offset = rng.startOffset; 14202 container = rng.startContainer; 14203 text = container.nodeValue; 14204 14205 caretContainer = getParentCaretContainer(selection.getStart()); 14206 if (caretContainer) { 14207 textNode = findFirstTextNode(caretContainer); 14208 } 14209 14210 // Expand to word is caret is in the middle of a text node and the char before/after is a alpha numeric character 14211 if (text && offset > 0 && offset < text.length && /\w/.test(text.charAt(offset)) && /\w/.test(text.charAt(offset - 1))) { 14212 // Get bookmark of caret position 14213 bookmark = selection.getBookmark(); 14214 14215 // Collapse bookmark range (WebKit) 14216 rng.collapse(true); 14217 14218 // Expand the range to the closest word and split it at those points 14219 rng = expandRng(rng, get(name)); 14220 rng = rangeUtils.split(rng); 14221 14222 // Apply the format to the range 14223 apply(name, vars, rng); 14224 14225 // Move selection back to caret position 14226 selection.moveToBookmark(bookmark); 14227 } else { 14228 if (!caretContainer || textNode.nodeValue !== INVISIBLE_CHAR) { 14229 caretContainer = createCaretContainer(true); 14230 textNode = caretContainer.firstChild; 14231 14232 rng.insertNode(caretContainer); 14233 offset = 1; 14234 14235 apply(name, vars, caretContainer); 14236 } else { 14237 apply(name, vars, caretContainer); 14238 } 14239 14240 // Move selection to text node 14241 selection.setCursorLocation(textNode, offset); 14242 } 14243 } 14244 14245 function removeCaretFormat() { 14246 var rng = selection.getRng(true), container, offset, bookmark, 14247 hasContentAfter, node, formatNode, parents = [], i, caretContainer; 14248 14249 container = rng.startContainer; 14250 offset = rng.startOffset; 14251 node = container; 14252 14253 if (container.nodeType == 3) { 14254 if (offset != container.nodeValue.length || container.nodeValue === INVISIBLE_CHAR) { 14255 hasContentAfter = true; 14256 } 14257 14258 node = node.parentNode; 14259 } 14260 14261 while (node) { 14262 if (matchNode(node, name, vars)) { 14263 formatNode = node; 14264 break; 14265 } 14266 14267 if (node.nextSibling) { 14268 hasContentAfter = true; 14269 } 14270 14271 parents.push(node); 14272 node = node.parentNode; 14273 } 14274 14275 // Node doesn't have the specified format 14276 if (!formatNode) { 14277 return; 14278 } 14279 14280 // Is there contents after the caret then remove the format on the element 14281 if (hasContentAfter) { 14282 // Get bookmark of caret position 14283 bookmark = selection.getBookmark(); 14284 14285 // Collapse bookmark range (WebKit) 14286 rng.collapse(true); 14287 14288 // Expand the range to the closest word and split it at those points 14289 rng = expandRng(rng, get(name), true); 14290 rng = rangeUtils.split(rng); 14291 14292 // Remove the format from the range 14293 remove(name, vars, rng); 14294 14295 // Move selection back to caret position 14296 selection.moveToBookmark(bookmark); 14297 } else { 14298 caretContainer = createCaretContainer(); 14299 14300 node = caretContainer; 14301 for (i = parents.length - 1; i >= 0; i--) { 14302 node.appendChild(dom.clone(parents[i], false)); 14303 node = node.firstChild; 14304 } 14305 14306 // Insert invisible character into inner most format element 14307 node.appendChild(dom.doc.createTextNode(INVISIBLE_CHAR)); 14308 node = node.firstChild; 14309 14310 var block = dom.getParent(formatNode, isTextBlock); 14311 14312 if (block && dom.isEmpty(block)) { 14313 // Replace formatNode with caretContainer when removing format from empty block like <p><b>|</b></p> 14314 formatNode.parentNode.replaceChild(caretContainer, formatNode); 14315 } else { 14316 // Insert caret container after the formated node 14317 dom.insertAfter(caretContainer, formatNode); 14318 } 14319 14320 // Move selection to text node 14321 selection.setCursorLocation(node, 1); 14322 14323 // If the formatNode is empty, we can remove it safely. 14324 if (dom.isEmpty(formatNode)) { 14325 dom.remove(formatNode); 14326 } 14327 } 14328 } 14329 14330 // Checks if the parent caret container node isn't empty if that is the case it 14331 // will remove the bogus state on all children that isn't empty 14332 function unmarkBogusCaretParents() { 14333 var caretContainer; 14334 14335 caretContainer = getParentCaretContainer(selection.getStart()); 14336 if (caretContainer && !dom.isEmpty(caretContainer)) { 14337 walk(caretContainer, function(node) { 14338 if (node.nodeType == 1 && node.id !== caretContainerId && !dom.isEmpty(node)) { 14339 dom.setAttrib(node, 'data-mce-bogus', null); 14340 } 14341 }, 'childNodes'); 14342 } 14343 } 14344 14345 // Only bind the caret events once 14346 if (!ed._hasCaretEvents) { 14347 // Mark current caret container elements as bogus when getting the contents so we don't end up with empty elements 14348 markCaretContainersBogus = function() { 14349 var nodes = [], i; 14350 14351 if (isCaretContainerEmpty(getParentCaretContainer(selection.getStart()), nodes)) { 14352 // Mark children 14353 i = nodes.length; 14354 while (i--) { 14355 dom.setAttrib(nodes[i], 'data-mce-bogus', '1'); 14356 } 14357 } 14358 }; 14359 14360 disableCaretContainer = function(e) { 14361 var keyCode = e.keyCode; 14362 14363 removeCaretContainer(); 14364 14365 // Remove caret container on keydown and it's a backspace, enter or left/right arrow keys 14366 if (keyCode == 8 || keyCode == 37 || keyCode == 39) { 14367 removeCaretContainer(getParentCaretContainer(selection.getStart())); 14368 } 14369 14370 unmarkBogusCaretParents(); 14371 }; 14372 14373 // Remove bogus state if they got filled by contents using editor.selection.setContent 14374 ed.on('SetContent', function(e) { 14375 if (e.selection) { 14376 unmarkBogusCaretParents(); 14377 } 14378 }); 14379 ed._hasCaretEvents = true; 14380 } 14381 14382 // Do apply or remove caret format 14383 if (type == "apply") { 14384 applyCaretFormat(); 14385 } else { 14386 removeCaretFormat(); 14387 } 14388 } 14389 14390 /** 14391 * Moves the start to the first suitable text node. 14392 */ 14393 function moveStart(rng) { 14394 var container = rng.startContainer, 14395 offset = rng.startOffset, isAtEndOfText, 14396 walker, node, nodes, tmpNode; 14397 14398 // Convert text node into index if possible 14399 if (container.nodeType == 3 && offset >= container.nodeValue.length) { 14400 // Get the parent container location and walk from there 14401 offset = nodeIndex(container); 14402 container = container.parentNode; 14403 isAtEndOfText = true; 14404 } 14405 14406 // Move startContainer/startOffset in to a suitable node 14407 if (container.nodeType == 1) { 14408 nodes = container.childNodes; 14409 container = nodes[Math.min(offset, nodes.length - 1)]; 14410 walker = new TreeWalker(container, dom.getParent(container, dom.isBlock)); 14411 14412 // If offset is at end of the parent node walk to the next one 14413 if (offset > nodes.length - 1 || isAtEndOfText) { 14414 walker.next(); 14415 } 14416 14417 for (node = walker.current(); node; node = walker.next()) { 14418 if (node.nodeType == 3 && !isWhiteSpaceNode(node)) { 14419 // IE has a "neat" feature where it moves the start node into the closest element 14420 // we can avoid this by inserting an element before it and then remove it after we set the selection 14421 tmpNode = dom.create('a', null, INVISIBLE_CHAR); 14422 node.parentNode.insertBefore(tmpNode, node); 14423 14424 // Set selection and remove tmpNode 14425 rng.setStart(node, 0); 14426 selection.setRng(rng); 14427 dom.remove(tmpNode); 14428 14429 return; 14430 } 14431 } 14432 } 14433 } 14434 }; 14435 }); 14436 14437 // Included from: js/tinymce/classes/UndoManager.js 14438 14439 /** 14440 * UndoManager.js 14441 * 14442 * Copyright, Moxiecode Systems AB 14443 * Released under LGPL License. 14444 * 14445 * License: http://www.tinymce.com/license 14446 * Contributing: http://www.tinymce.com/contributing 14447 */ 14448 14449 /** 14450 * This class handles the undo/redo history levels for the editor. Since the build in undo/redo has major drawbacks a custom one was needed. 14451 * 14452 * @class tinymce.UndoManager 14453 */ 14454 define("tinymce/UndoManager", [ 14455 "tinymce/Env", 14456 "tinymce/util/Tools" 14457 ], function(Env, Tools) { 14458 var trim = Tools.trim, trimContentRegExp; 14459 14460 trimContentRegExp = new RegExp([ 14461 '<span[^>]+data-mce-bogus[^>]+>[\u200B\uFEFF]+<\\/span>', // Trim bogus spans like caret containers 14462 '<div[^>]+data-mce-bogus[^>]+><\\/div>', // Trim bogus divs like resize handles 14463 '\\s?data-mce-selected="[^"]+"' // Trim temporaty data-mce prefixed attributes like data-mce-selected 14464 ].join('|'), 'gi'); 14465 14466 return function(editor) { 14467 var self = this, index = 0, data = [], beforeBookmark, isFirstTypedCharacter, locks = 0; 14468 14469 // Returns a trimmed version of the current editor contents 14470 function getContent() { 14471 return trim(editor.getContent({format: 'raw', no_events: 1}).replace(trimContentRegExp, '')); 14472 } 14473 14474 function addNonTypingUndoLevel(e) { 14475 self.typing = false; 14476 self.add({}, e); 14477 } 14478 14479 // Add initial undo level when the editor is initialized 14480 editor.on('init', function() { 14481 self.add(); 14482 }); 14483 14484 // Get position before an execCommand is processed 14485 editor.on('BeforeExecCommand', function(e) { 14486 var cmd = e.command; 14487 14488 if (cmd != 'Undo' && cmd != 'Redo' && cmd != 'mceRepaint') { 14489 self.beforeChange(); 14490 } 14491 }); 14492 14493 // Add undo level after an execCommand call was made 14494 editor.on('ExecCommand', function(e) { 14495 var cmd = e.command; 14496 14497 if (cmd != 'Undo' && cmd != 'Redo' && cmd != 'mceRepaint') { 14498 addNonTypingUndoLevel(e); 14499 } 14500 }); 14501 14502 editor.on('ObjectResizeStart', function() { 14503 self.beforeChange(); 14504 }); 14505 14506 editor.on('SaveContent ObjectResized blur', addNonTypingUndoLevel); 14507 editor.on('DragEnd', addNonTypingUndoLevel); 14508 14509 editor.on('KeyUp', function(e) { 14510 var keyCode = e.keyCode; 14511 14512 if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode == 45 || keyCode == 13 || e.ctrlKey) { 14513 addNonTypingUndoLevel(); 14514 editor.nodeChanged(); 14515 } 14516 14517 if (keyCode == 46 || keyCode == 8 || (Env.mac && (keyCode == 91 || keyCode == 93))) { 14518 editor.nodeChanged(); 14519 } 14520 14521 // Fire a TypingUndo event on the first character entered 14522 if (isFirstTypedCharacter && self.typing) { 14523 // Make the it dirty if the content was changed after typing the first character 14524 if (!editor.isDirty()) { 14525 editor.isNotDirty = !data[0] || getContent() == data[0].content; 14526 14527 // Fire initial change event 14528 if (!editor.isNotDirty) { 14529 editor.fire('change', {level: data[0], lastLevel: null}); 14530 } 14531 } 14532 14533 editor.fire('TypingUndo'); 14534 isFirstTypedCharacter = false; 14535 editor.nodeChanged(); 14536 } 14537 }); 14538 14539 editor.on('KeyDown', function(e) { 14540 var keyCode = e.keyCode; 14541 14542 // Is caracter positon keys left,right,up,down,home,end,pgdown,pgup,enter 14543 if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode == 45) { 14544 if (self.typing) { 14545 addNonTypingUndoLevel(e); 14546 } 14547 14548 return; 14549 } 14550 14551 // If key isn't shift,ctrl,alt,capslock,metakey 14552 if ((keyCode < 16 || keyCode > 20) && keyCode != 224 && keyCode != 91 && !self.typing) { 14553 self.beforeChange(); 14554 self.typing = true; 14555 self.add({}, e); 14556 isFirstTypedCharacter = true; 14557 } 14558 }); 14559 14560 editor.on('MouseDown', function(e) { 14561 if (self.typing) { 14562 addNonTypingUndoLevel(e); 14563 } 14564 }); 14565 14566 // Add keyboard shortcuts for undo/redo keys 14567 editor.addShortcut('ctrl+z', '', 'Undo'); 14568 editor.addShortcut('ctrl+y,ctrl+shift+z', '', 'Redo'); 14569 14570 editor.on('AddUndo Undo Redo ClearUndos MouseUp', function(e) { 14571 if (!e.isDefaultPrevented()) { 14572 editor.nodeChanged(); 14573 } 14574 }); 14575 14576 self = { 14577 // Explose for debugging reasons 14578 data: data, 14579 14580 /** 14581 * State if the user is currently typing or not. This will add a typing operation into one undo 14582 * level instead of one new level for each keystroke. 14583 * 14584 * @field {Boolean} typing 14585 */ 14586 typing: false, 14587 14588 /** 14589 * Stores away a bookmark to be used when performing an undo action so that the selection is before 14590 * the change has been made. 14591 * 14592 * @method beforeChange 14593 */ 14594 beforeChange: function() { 14595 if (!locks) { 14596 beforeBookmark = editor.selection.getBookmark(2, true); 14597 } 14598 }, 14599 14600 /** 14601 * Adds a new undo level/snapshot to the undo list. 14602 * 14603 * @method add 14604 * @param {Object} level Optional undo level object to add. 14605 * @param {DOMEvent} Event Optional event responsible for the creation of the undo level. 14606 * @return {Object} Undo level that got added or null it a level wasn't needed. 14607 */ 14608 add: function(level, event) { 14609 var i, settings = editor.settings, lastLevel; 14610 14611 level = level || {}; 14612 level.content = getContent(); 14613 14614 if (locks || editor.removed) { 14615 return null; 14616 } 14617 14618 lastLevel = data[index]; 14619 if (editor.fire('BeforeAddUndo', {level: level, lastLevel: lastLevel, originalEvent: event}).isDefaultPrevented()) { 14620 return null; 14621 } 14622 14623 // Add undo level if needed 14624 if (lastLevel && lastLevel.content == level.content) { 14625 return null; 14626 } 14627 14628 // Set before bookmark on previous level 14629 if (data[index]) { 14630 data[index].beforeBookmark = beforeBookmark; 14631 } 14632 14633 // Time to compress 14634 if (settings.custom_undo_redo_levels) { 14635 if (data.length > settings.custom_undo_redo_levels) { 14636 for (i = 0; i < data.length - 1; i++) { 14637 data[i] = data[i + 1]; 14638 } 14639 14640 data.length--; 14641 index = data.length; 14642 } 14643 } 14644 14645 // Get a non intrusive normalized bookmark 14646 level.bookmark = editor.selection.getBookmark(2, true); 14647 14648 // Crop array if needed 14649 if (index < data.length - 1) { 14650 data.length = index + 1; 14651 } 14652 14653 data.push(level); 14654 index = data.length - 1; 14655 14656 var args = {level: level, lastLevel: lastLevel, originalEvent: event}; 14657 14658 editor.fire('AddUndo', args); 14659 14660 if (index > 0) { 14661 editor.isNotDirty = false; 14662 editor.fire('change', args); 14663 } 14664 14665 return level; 14666 }, 14667 14668 /** 14669 * Undoes the last action. 14670 * 14671 * @method undo 14672 * @return {Object} Undo level or null if no undo was performed. 14673 */ 14674 undo: function() { 14675 var level; 14676 14677 if (self.typing) { 14678 self.add(); 14679 self.typing = false; 14680 } 14681 14682 if (index > 0) { 14683 level = data[--index]; 14684 14685 // Undo to first index then set dirty state to false 14686 if (index === 0) { 14687 editor.isNotDirty = true; 14688 } 14689 14690 editor.setContent(level.content, {format: 'raw'}); 14691 editor.selection.moveToBookmark(level.beforeBookmark); 14692 14693 editor.fire('undo', {level: level}); 14694 } 14695 14696 return level; 14697 }, 14698 14699 /** 14700 * Redoes the last action. 14701 * 14702 * @method redo 14703 * @return {Object} Redo level or null if no redo was performed. 14704 */ 14705 redo: function() { 14706 var level; 14707 14708 if (index < data.length - 1) { 14709 level = data[++index]; 14710 14711 editor.setContent(level.content, {format: 'raw'}); 14712 editor.selection.moveToBookmark(level.bookmark); 14713 14714 editor.fire('redo', {level: level}); 14715 } 14716 14717 return level; 14718 }, 14719 14720 /** 14721 * Removes all undo levels. 14722 * 14723 * @method clear 14724 */ 14725 clear: function() { 14726 data = []; 14727 index = 0; 14728 self.typing = false; 14729 editor.fire('ClearUndos'); 14730 }, 14731 14732 /** 14733 * Returns true/false if the undo manager has any undo levels. 14734 * 14735 * @method hasUndo 14736 * @return {Boolean} true/false if the undo manager has any undo levels. 14737 */ 14738 hasUndo: function() { 14739 // Has undo levels or typing and content isn't the same as the initial level 14740 return index > 0 || (self.typing && data[0] && getContent() != data[0].content); 14741 }, 14742 14743 /** 14744 * Returns true/false if the undo manager has any redo levels. 14745 * 14746 * @method hasRedo 14747 * @return {Boolean} true/false if the undo manager has any redo levels. 14748 */ 14749 hasRedo: function() { 14750 return index < data.length - 1 && !this.typing; 14751 }, 14752 14753 /** 14754 * Executes the specified function in an undo transation. The selection 14755 * before the modification will be stored to the undo stack and if the DOM changes 14756 * it will add a new undo level. Any methods within the transation that adds undo levels will 14757 * be ignored. So a transation can include calls to execCommand or editor.insertContent. 14758 * 14759 * @method transact 14760 * @param {function} callback Function to execute dom manipulation logic in. 14761 */ 14762 transact: function(callback) { 14763 self.beforeChange(); 14764 14765 try { 14766 locks++; 14767 callback(); 14768 } finally { 14769 locks--; 14770 } 14771 14772 self.add(); 14773 } 14774 }; 14775 14776 return self; 14777 }; 14778 }); 14779 14780 // Included from: js/tinymce/classes/EnterKey.js 14781 14782 /** 14783 * EnterKey.js 14784 * 14785 * Copyright, Moxiecode Systems AB 14786 * Released under LGPL License. 14787 * 14788 * License: http://www.tinymce.com/license 14789 * Contributing: http://www.tinymce.com/contributing 14790 */ 14791 14792 /** 14793 * Contains logic for handling the enter key to split/generate block elements. 14794 */ 14795 define("tinymce/EnterKey", [ 14796 "tinymce/dom/TreeWalker", 14797 "tinymce/dom/RangeUtils", 14798 "tinymce/Env" 14799 ], function(TreeWalker, RangeUtils, Env) { 14800 var isIE = Env.ie && Env.ie < 11; 14801 14802 return function(editor) { 14803 var dom = editor.dom, selection = editor.selection, settings = editor.settings; 14804 var undoManager = editor.undoManager, schema = editor.schema, nonEmptyElementsMap = schema.getNonEmptyElements(); 14805 14806 function handleEnterKey(evt) { 14807 var rng, tmpRng, editableRoot, container, offset, parentBlock, documentMode, shiftKey, 14808 newBlock, fragment, containerBlock, parentBlockName, containerBlockName, newBlockName, isAfterLastNodeInContainer; 14809 14810 // Returns true if the block can be split into two blocks or not 14811 function canSplitBlock(node) { 14812 return node && 14813 dom.isBlock(node) && 14814 !/^(TD|TH|CAPTION|FORM)$/.test(node.nodeName) && 14815 !/^(fixed|absolute)/i.test(node.style.position) && 14816 dom.getContentEditable(node) !== "true"; 14817 } 14818 14819 // Renders empty block on IE 14820 function renderBlockOnIE(block) { 14821 var oldRng; 14822 14823 if (dom.isBlock(block)) { 14824 oldRng = selection.getRng(); 14825 block.appendChild(dom.create('span', null, '\u00a0')); 14826 selection.select(block); 14827 block.lastChild.outerHTML = ''; 14828 selection.setRng(oldRng); 14829 } 14830 } 14831 14832 // Remove the first empty inline element of the block so this: <p><b><em></em></b>x</p> becomes this: <p>x</p> 14833 function trimInlineElementsOnLeftSideOfBlock(block) { 14834 var node = block, firstChilds = [], i; 14835 14836 // Find inner most first child ex: <p><i><b>*</b></i></p> 14837 while ((node = node.firstChild)) { 14838 if (dom.isBlock(node)) { 14839 return; 14840 } 14841 14842 if (node.nodeType == 1 && !nonEmptyElementsMap[node.nodeName.toLowerCase()]) { 14843 firstChilds.push(node); 14844 } 14845 } 14846 14847 i = firstChilds.length; 14848 while (i--) { 14849 node = firstChilds[i]; 14850 if (!node.hasChildNodes() || (node.firstChild == node.lastChild && node.firstChild.nodeValue === '')) { 14851 dom.remove(node); 14852 } else { 14853 // Remove <a> </a> see #5381 14854 if (node.nodeName == "A" && (node.innerText || node.textContent) === ' ') { 14855 dom.remove(node); 14856 } 14857 } 14858 } 14859 } 14860 14861 // Moves the caret to a suitable position within the root for example in the first non 14862 // pure whitespace text node or before an image 14863 function moveToCaretPosition(root) { 14864 var walker, node, rng, lastNode = root, tempElm; 14865 14866 function firstNonWhiteSpaceNodeSibling(node) { 14867 while (node) { 14868 if (node.nodeType == 1 || (node.nodeType == 3 && node.data && /[\r\n\s]/.test(node.data))) { 14869 return node; 14870 } 14871 14872 node = node.nextSibling; 14873 } 14874 } 14875 14876 // Old IE versions doesn't properly render blocks with br elements in them 14877 // For example <p><br></p> wont be rendered correctly in a contentEditable area 14878 // until you remove the br producing <p></p> 14879 if (Env.ie && Env.ie < 9 && parentBlock && parentBlock.firstChild) { 14880 if (parentBlock.firstChild == parentBlock.lastChild && parentBlock.firstChild.tagName == 'BR') { 14881 dom.remove(parentBlock.firstChild); 14882 } 14883 } 14884 14885 if (root.nodeName == 'LI') { 14886 var firstChild = firstNonWhiteSpaceNodeSibling(root.firstChild); 14887 14888 if (firstChild && /^(UL|OL)$/.test(firstChild.nodeName)) { 14889 root.insertBefore(dom.doc.createTextNode('\u00a0'), root.firstChild); 14890 } 14891 } 14892 14893 rng = dom.createRng(); 14894 14895 if (root.hasChildNodes()) { 14896 walker = new TreeWalker(root, root); 14897 14898 while ((node = walker.current())) { 14899 if (node.nodeType == 3) { 14900 rng.setStart(node, 0); 14901 rng.setEnd(node, 0); 14902 break; 14903 } 14904 14905 if (nonEmptyElementsMap[node.nodeName.toLowerCase()]) { 14906 rng.setStartBefore(node); 14907 rng.setEndBefore(node); 14908 break; 14909 } 14910 14911 lastNode = node; 14912 node = walker.next(); 14913 } 14914 14915 if (!node) { 14916 rng.setStart(lastNode, 0); 14917 rng.setEnd(lastNode, 0); 14918 } 14919 } else { 14920 if (root.nodeName == 'BR') { 14921 if (root.nextSibling && dom.isBlock(root.nextSibling)) { 14922 // Trick on older IE versions to render the caret before the BR between two lists 14923 if (!documentMode || documentMode < 9) { 14924 tempElm = dom.create('br'); 14925 root.parentNode.insertBefore(tempElm, root); 14926 } 14927 14928 rng.setStartBefore(root); 14929 rng.setEndBefore(root); 14930 } else { 14931 rng.setStartAfter(root); 14932 rng.setEndAfter(root); 14933 } 14934 } else { 14935 rng.setStart(root, 0); 14936 rng.setEnd(root, 0); 14937 } 14938 } 14939 14940 selection.setRng(rng); 14941 14942 // Remove tempElm created for old IE:s 14943 dom.remove(tempElm); 14944 selection.scrollIntoView(root); 14945 } 14946 14947 function setForcedBlockAttrs(node) { 14948 var forcedRootBlockName = settings.forced_root_block; 14949 14950 if (forcedRootBlockName && forcedRootBlockName.toLowerCase() === node.tagName.toLowerCase()) { 14951 dom.setAttribs(node, settings.forced_root_block_attrs); 14952 } 14953 } 14954 14955 // Creates a new block element by cloning the current one or creating a new one if the name is specified 14956 // This function will also copy any text formatting from the parent block and add it to the new one 14957 function createNewBlock(name) { 14958 var node = container, block, clonedNode, caretNode; 14959 14960 if (name || parentBlockName == "TABLE") { 14961 block = dom.create(name || newBlockName); 14962 setForcedBlockAttrs(block); 14963 } else { 14964 block = parentBlock.cloneNode(false); 14965 } 14966 14967 caretNode = block; 14968 14969 // Clone any parent styles 14970 if (settings.keep_styles !== false) { 14971 do { 14972 if (/^(SPAN|STRONG|B|EM|I|FONT|STRIKE|U|VAR|CITE|DFN|CODE|MARK|Q|SUP|SUB|SAMP)$/.test(node.nodeName)) { 14973 // Never clone a caret containers 14974 if (node.id == '_mce_caret') { 14975 continue; 14976 } 14977 14978 clonedNode = node.cloneNode(false); 14979 dom.setAttrib(clonedNode, 'id', ''); // Remove ID since it needs to be document unique 14980 14981 if (block.hasChildNodes()) { 14982 clonedNode.appendChild(block.firstChild); 14983 block.appendChild(clonedNode); 14984 } else { 14985 caretNode = clonedNode; 14986 block.appendChild(clonedNode); 14987 } 14988 } 14989 } while ((node = node.parentNode)); 14990 } 14991 14992 // BR is needed in empty blocks on non IE browsers 14993 if (!isIE) { 14994 caretNode.innerHTML = '<br data-mce-bogus="1">'; 14995 } 14996 14997 return block; 14998 } 14999 15000 // Returns true/false if the caret is at the start/end of the parent block element 15001 function isCaretAtStartOrEndOfBlock(start) { 15002 var walker, node, name; 15003 15004 // Caret is in the middle of a text node like "a|b" 15005 if (container.nodeType == 3 && (start ? offset > 0 : offset < container.nodeValue.length)) { 15006 return false; 15007 } 15008 15009 // If after the last element in block node edge case for #5091 15010 if (container.parentNode == parentBlock && isAfterLastNodeInContainer && !start) { 15011 return true; 15012 } 15013 15014 // If the caret if before the first element in parentBlock 15015 if (start && container.nodeType == 1 && container == parentBlock.firstChild) { 15016 return true; 15017 } 15018 15019 // Caret can be before/after a table 15020 if (container.nodeName === "TABLE" || (container.previousSibling && container.previousSibling.nodeName == "TABLE")) { 15021 return (isAfterLastNodeInContainer && !start) || (!isAfterLastNodeInContainer && start); 15022 } 15023 15024 // Walk the DOM and look for text nodes or non empty elements 15025 walker = new TreeWalker(container, parentBlock); 15026 15027 // If caret is in beginning or end of a text block then jump to the next/previous node 15028 if (container.nodeType == 3) { 15029 if (start && offset === 0) { 15030 walker.prev(); 15031 } else if (!start && offset == container.nodeValue.length) { 15032 walker.next(); 15033 } 15034 } 15035 15036 while ((node = walker.current())) { 15037 if (node.nodeType === 1) { 15038 // Ignore bogus elements 15039 if (!node.getAttribute('data-mce-bogus')) { 15040 // Keep empty elements like <img /> <input /> but not trailing br:s like <p>text|<br></p> 15041 name = node.nodeName.toLowerCase(); 15042 if (nonEmptyElementsMap[name] && name !== 'br') { 15043 return false; 15044 } 15045 } 15046 } else if (node.nodeType === 3 && !/^[ \t\r\n]*$/.test(node.nodeValue)) { 15047 return false; 15048 } 15049 15050 if (start) { 15051 walker.prev(); 15052 } else { 15053 walker.next(); 15054 } 15055 } 15056 15057 return true; 15058 } 15059 15060 // Wraps any text nodes or inline elements in the specified forced root block name 15061 function wrapSelfAndSiblingsInDefaultBlock(container, offset) { 15062 var newBlock, parentBlock, startNode, node, next, rootBlockName, blockName = newBlockName || 'P'; 15063 15064 // Not in a block element or in a table cell or caption 15065 parentBlock = dom.getParent(container, dom.isBlock); 15066 rootBlockName = editor.getBody().nodeName.toLowerCase(); 15067 if (!parentBlock || !canSplitBlock(parentBlock)) { 15068 parentBlock = parentBlock || editableRoot; 15069 15070 if (!parentBlock.hasChildNodes()) { 15071 newBlock = dom.create(blockName); 15072 setForcedBlockAttrs(newBlock); 15073 parentBlock.appendChild(newBlock); 15074 rng.setStart(newBlock, 0); 15075 rng.setEnd(newBlock, 0); 15076 return newBlock; 15077 } 15078 15079 // Find parent that is the first child of parentBlock 15080 node = container; 15081 while (node.parentNode != parentBlock) { 15082 node = node.parentNode; 15083 } 15084 15085 // Loop left to find start node start wrapping at 15086 while (node && !dom.isBlock(node)) { 15087 startNode = node; 15088 node = node.previousSibling; 15089 } 15090 15091 if (startNode && schema.isValidChild(rootBlockName, blockName.toLowerCase())) { 15092 newBlock = dom.create(blockName); 15093 setForcedBlockAttrs(newBlock); 15094 startNode.parentNode.insertBefore(newBlock, startNode); 15095 15096 // Start wrapping until we hit a block 15097 node = startNode; 15098 while (node && !dom.isBlock(node)) { 15099 next = node.nextSibling; 15100 newBlock.appendChild(node); 15101 node = next; 15102 } 15103 15104 // Restore range to it's past location 15105 rng.setStart(container, offset); 15106 rng.setEnd(container, offset); 15107 } 15108 } 15109 15110 return container; 15111 } 15112 15113 // Inserts a block or br before/after or in the middle of a split list of the LI is empty 15114 function handleEmptyListItem() { 15115 function isFirstOrLastLi(first) { 15116 var node = containerBlock[first ? 'firstChild' : 'lastChild']; 15117 15118 // Find first/last element since there might be whitespace there 15119 while (node) { 15120 if (node.nodeType == 1) { 15121 break; 15122 } 15123 15124 node = node[first ? 'nextSibling' : 'previousSibling']; 15125 } 15126 15127 return node === parentBlock; 15128 } 15129 15130 function getContainerBlock() { 15131 var containerBlockParent = containerBlock.parentNode; 15132 15133 if (containerBlockParent.nodeName == 'LI') { 15134 return containerBlockParent; 15135 } 15136 15137 return containerBlock; 15138 } 15139 15140 // Check if we are in an nested list 15141 var containerBlockParentName = containerBlock.parentNode.nodeName; 15142 if (/^(OL|UL|LI)$/.test(containerBlockParentName)) { 15143 newBlockName = 'LI'; 15144 } 15145 15146 newBlock = newBlockName ? createNewBlock(newBlockName) : dom.create('BR'); 15147 15148 if (isFirstOrLastLi(true) && isFirstOrLastLi()) { 15149 if (containerBlockParentName == 'LI') { 15150 // Nested list is inside a LI 15151 dom.insertAfter(newBlock, getContainerBlock()); 15152 } else { 15153 // Is first and last list item then replace the OL/UL with a text block 15154 dom.replace(newBlock, containerBlock); 15155 } 15156 } else if (isFirstOrLastLi(true)) { 15157 if (containerBlockParentName == 'LI') { 15158 // List nested in an LI then move the list to a new sibling LI 15159 dom.insertAfter(newBlock, getContainerBlock()); 15160 newBlock.appendChild(dom.doc.createTextNode(' ')); // Needed for IE so the caret can be placed 15161 newBlock.appendChild(containerBlock); 15162 } else { 15163 // First LI in list then remove LI and add text block before list 15164 containerBlock.parentNode.insertBefore(newBlock, containerBlock); 15165 } 15166 } else if (isFirstOrLastLi()) { 15167 // Last LI in list then remove LI and add text block after list 15168 dom.insertAfter(newBlock, getContainerBlock()); 15169 renderBlockOnIE(newBlock); 15170 } else { 15171 // Middle LI in list the split the list and insert a text block in the middle 15172 // Extract after fragment and insert it after the current block 15173 containerBlock = getContainerBlock(); 15174 tmpRng = rng.cloneRange(); 15175 tmpRng.setStartAfter(parentBlock); 15176 tmpRng.setEndAfter(containerBlock); 15177 fragment = tmpRng.extractContents(); 15178 15179 if (newBlockName == 'LI' && fragment.firstChild.nodeName == 'LI') { 15180 newBlock = fragment.firstChild; 15181 dom.insertAfter(fragment, containerBlock); 15182 } else { 15183 dom.insertAfter(fragment, containerBlock); 15184 dom.insertAfter(newBlock, containerBlock); 15185 } 15186 } 15187 15188 dom.remove(parentBlock); 15189 moveToCaretPosition(newBlock); 15190 undoManager.add(); 15191 } 15192 15193 // Walks the parent block to the right and look for BR elements 15194 function hasRightSideContent() { 15195 var walker = new TreeWalker(container, parentBlock), node; 15196 15197 while ((node = walker.next())) { 15198 if (nonEmptyElementsMap[node.nodeName.toLowerCase()] || node.length > 0) { 15199 return true; 15200 } 15201 } 15202 } 15203 15204 // Inserts a BR element if the forced_root_block option is set to false or empty string 15205 function insertBr() { 15206 var brElm, extraBr, marker; 15207 15208 if (container && container.nodeType == 3 && offset >= container.nodeValue.length) { 15209 // Insert extra BR element at the end block elements 15210 if (!isIE && !hasRightSideContent()) { 15211 brElm = dom.create('br'); 15212 rng.insertNode(brElm); 15213 rng.setStartAfter(brElm); 15214 rng.setEndAfter(brElm); 15215 extraBr = true; 15216 } 15217 } 15218 15219 brElm = dom.create('br'); 15220 rng.insertNode(brElm); 15221 15222 // Rendering modes below IE8 doesn't display BR elements in PRE unless we have a \n before it 15223 if (isIE && parentBlockName == 'PRE' && (!documentMode || documentMode < 8)) { 15224 brElm.parentNode.insertBefore(dom.doc.createTextNode('\r'), brElm); 15225 } 15226 15227 // Insert temp marker and scroll to that 15228 marker = dom.create('span', {}, ' '); 15229 brElm.parentNode.insertBefore(marker, brElm); 15230 selection.scrollIntoView(marker); 15231 dom.remove(marker); 15232 15233 if (!extraBr) { 15234 rng.setStartAfter(brElm); 15235 rng.setEndAfter(brElm); 15236 } else { 15237 rng.setStartBefore(brElm); 15238 rng.setEndBefore(brElm); 15239 } 15240 15241 selection.setRng(rng); 15242 undoManager.add(); 15243 } 15244 15245 // Trims any linebreaks at the beginning of node user for example when pressing enter in a PRE element 15246 function trimLeadingLineBreaks(node) { 15247 do { 15248 if (node.nodeType === 3) { 15249 node.nodeValue = node.nodeValue.replace(/^[\r\n]+/, ''); 15250 } 15251 15252 node = node.firstChild; 15253 } while (node); 15254 } 15255 15256 function getEditableRoot(node) { 15257 var root = dom.getRoot(), parent, editableRoot; 15258 15259 // Get all parents until we hit a non editable parent or the root 15260 parent = node; 15261 while (parent !== root && dom.getContentEditable(parent) !== "false") { 15262 if (dom.getContentEditable(parent) === "true") { 15263 editableRoot = parent; 15264 } 15265 15266 parent = parent.parentNode; 15267 } 15268 15269 return parent !== root ? editableRoot : root; 15270 } 15271 15272 // Adds a BR at the end of blocks that only contains an IMG or INPUT since 15273 // these might be floated and then they won't expand the block 15274 function addBrToBlockIfNeeded(block) { 15275 var lastChild; 15276 15277 // IE will render the blocks correctly other browsers needs a BR 15278 if (!isIE) { 15279 block.normalize(); // Remove empty text nodes that got left behind by the extract 15280 15281 // Check if the block is empty or contains a floated last child 15282 lastChild = block.lastChild; 15283 if (!lastChild || (/^(left|right)$/gi.test(dom.getStyle(lastChild, 'float', true)))) { 15284 dom.add(block, 'br'); 15285 } 15286 } 15287 } 15288 15289 rng = selection.getRng(true); 15290 15291 // Event is blocked by some other handler for example the lists plugin 15292 if (evt.isDefaultPrevented()) { 15293 return; 15294 } 15295 15296 // Delete any selected contents 15297 if (!rng.collapsed) { 15298 editor.execCommand('Delete'); 15299 return; 15300 } 15301 15302 // Setup range items and newBlockName 15303 new RangeUtils(dom).normalize(rng); 15304 container = rng.startContainer; 15305 offset = rng.startOffset; 15306 newBlockName = (settings.force_p_newlines ? 'p' : '') || settings.forced_root_block; 15307 newBlockName = newBlockName ? newBlockName.toUpperCase() : ''; 15308 documentMode = dom.doc.documentMode; 15309 shiftKey = evt.shiftKey; 15310 15311 // Resolve node index 15312 if (container.nodeType == 1 && container.hasChildNodes()) { 15313 isAfterLastNodeInContainer = offset > container.childNodes.length - 1; 15314 15315 container = container.childNodes[Math.min(offset, container.childNodes.length - 1)] || container; 15316 if (isAfterLastNodeInContainer && container.nodeType == 3) { 15317 offset = container.nodeValue.length; 15318 } else { 15319 offset = 0; 15320 } 15321 } 15322 15323 // Get editable root node normaly the body element but sometimes a div or span 15324 editableRoot = getEditableRoot(container); 15325 15326 // If there is no editable root then enter is done inside a contentEditable false element 15327 if (!editableRoot) { 15328 return; 15329 } 15330 15331 undoManager.beforeChange(); 15332 15333 // If editable root isn't block nor the root of the editor 15334 if (!dom.isBlock(editableRoot) && editableRoot != dom.getRoot()) { 15335 if (!newBlockName || shiftKey) { 15336 insertBr(); 15337 } 15338 15339 return; 15340 } 15341 15342 // Wrap the current node and it's sibling in a default block if it's needed. 15343 // for example this <td>text|<b>text2</b></td> will become this <td><p>text|<b>text2</p></b></td> 15344 // This won't happen if root blocks are disabled or the shiftKey is pressed 15345 if ((newBlockName && !shiftKey) || (!newBlockName && shiftKey)) { 15346 container = wrapSelfAndSiblingsInDefaultBlock(container, offset); 15347 } 15348 15349 // Find parent block and setup empty block paddings 15350 parentBlock = dom.getParent(container, dom.isBlock); 15351 containerBlock = parentBlock ? dom.getParent(parentBlock.parentNode, dom.isBlock) : null; 15352 15353 // Setup block names 15354 parentBlockName = parentBlock ? parentBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5 15355 containerBlockName = containerBlock ? containerBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5 15356 15357 // Enter inside block contained within a LI then split or insert before/after LI 15358 if (containerBlockName == 'LI' && !evt.ctrlKey) { 15359 parentBlock = containerBlock; 15360 parentBlockName = containerBlockName; 15361 } 15362 15363 // Handle enter in LI 15364 if (parentBlockName == 'LI') { 15365 if (!newBlockName && shiftKey) { 15366 insertBr(); 15367 return; 15368 } 15369 15370 // Handle enter inside an empty list item 15371 if (dom.isEmpty(parentBlock)) { 15372 handleEmptyListItem(); 15373 return; 15374 } 15375 } 15376 15377 // Don't split PRE tags but insert a BR instead easier when writing code samples etc 15378 if (parentBlockName == 'PRE' && settings.br_in_pre !== false) { 15379 if (!shiftKey) { 15380 insertBr(); 15381 return; 15382 } 15383 } else { 15384 // If no root block is configured then insert a BR by default or if the shiftKey is pressed 15385 if ((!newBlockName && !shiftKey && parentBlockName != 'LI') || (newBlockName && shiftKey)) { 15386 insertBr(); 15387 return; 15388 } 15389 } 15390 15391 // If parent block is root then never insert new blocks 15392 if (newBlockName && parentBlock === editor.getBody()) { 15393 return; 15394 } 15395 15396 // Default block name if it's not configured 15397 newBlockName = newBlockName || 'P'; 15398 15399 // Insert new block before/after the parent block depending on caret location 15400 if (isCaretAtStartOrEndOfBlock()) { 15401 // If the caret is at the end of a header we produce a P tag after it similar to Word unless we are in a hgroup 15402 if (/^(H[1-6]|PRE|FIGURE)$/.test(parentBlockName) && containerBlockName != 'HGROUP') { 15403 newBlock = createNewBlock(newBlockName); 15404 } else { 15405 newBlock = createNewBlock(); 15406 } 15407 15408 // Split the current container block element if enter is pressed inside an empty inner block element 15409 if (settings.end_container_on_empty_block && canSplitBlock(containerBlock) && dom.isEmpty(parentBlock)) { 15410 // Split container block for example a BLOCKQUOTE at the current blockParent location for example a P 15411 newBlock = dom.split(containerBlock, parentBlock); 15412 } else { 15413 dom.insertAfter(newBlock, parentBlock); 15414 } 15415 15416 moveToCaretPosition(newBlock); 15417 } else if (isCaretAtStartOrEndOfBlock(true)) { 15418 // Insert new block before 15419 newBlock = parentBlock.parentNode.insertBefore(createNewBlock(), parentBlock); 15420 renderBlockOnIE(newBlock); 15421 moveToCaretPosition(parentBlock); 15422 } else { 15423 // Extract after fragment and insert it after the current block 15424 tmpRng = rng.cloneRange(); 15425 tmpRng.setEndAfter(parentBlock); 15426 fragment = tmpRng.extractContents(); 15427 trimLeadingLineBreaks(fragment); 15428 newBlock = fragment.firstChild; 15429 dom.insertAfter(fragment, parentBlock); 15430 trimInlineElementsOnLeftSideOfBlock(newBlock); 15431 addBrToBlockIfNeeded(parentBlock); 15432 moveToCaretPosition(newBlock); 15433 } 15434 15435 dom.setAttrib(newBlock, 'id', ''); // Remove ID since it needs to be document unique 15436 15437 // Allow custom handling of new blocks 15438 editor.fire('NewBlock', { newBlock: newBlock }); 15439 15440 undoManager.add(); 15441 } 15442 15443 editor.on('keydown', function(evt) { 15444 if (evt.keyCode == 13) { 15445 if (handleEnterKey(evt) !== false) { 15446 evt.preventDefault(); 15447 } 15448 } 15449 }); 15450 }; 15451 }); 15452 15453 // Included from: js/tinymce/classes/ForceBlocks.js 15454 15455 /** 15456 * ForceBlocks.js 15457 * 15458 * Copyright, Moxiecode Systems AB 15459 * Released under LGPL License. 15460 * 15461 * License: http://www.tinymce.com/license 15462 * Contributing: http://www.tinymce.com/contributing 15463 */ 15464 15465 define("tinymce/ForceBlocks", [], function() { 15466 return function(editor) { 15467 var settings = editor.settings, dom = editor.dom, selection = editor.selection; 15468 var schema = editor.schema, blockElements = schema.getBlockElements(); 15469 15470 function addRootBlocks() { 15471 var node = selection.getStart(), rootNode = editor.getBody(), rng; 15472 var startContainer, startOffset, endContainer, endOffset, rootBlockNode; 15473 var tempNode, offset = -0xFFFFFF, wrapped, restoreSelection; 15474 var tmpRng, rootNodeName, forcedRootBlock; 15475 15476 forcedRootBlock = settings.forced_root_block; 15477 15478 if (!node || node.nodeType !== 1 || !forcedRootBlock) { 15479 return; 15480 } 15481 15482 // Check if node is wrapped in block 15483 while (node && node != rootNode) { 15484 if (blockElements[node.nodeName]) { 15485 return; 15486 } 15487 15488 node = node.parentNode; 15489 } 15490 15491 // Get current selection 15492 rng = selection.getRng(); 15493 if (rng.setStart) { 15494 startContainer = rng.startContainer; 15495 startOffset = rng.startOffset; 15496 endContainer = rng.endContainer; 15497 endOffset = rng.endOffset; 15498 15499 try { 15500 restoreSelection = editor.getDoc().activeElement === rootNode; 15501 } catch (ex) { 15502 // IE throws unspecified error here sometimes 15503 } 15504 } else { 15505 // Force control range into text range 15506 if (rng.item) { 15507 node = rng.item(0); 15508 rng = editor.getDoc().body.createTextRange(); 15509 rng.moveToElementText(node); 15510 } 15511 15512 restoreSelection = rng.parentElement().ownerDocument === editor.getDoc(); 15513 tmpRng = rng.duplicate(); 15514 tmpRng.collapse(true); 15515 startOffset = tmpRng.move('character', offset) * -1; 15516 15517 if (!tmpRng.collapsed) { 15518 tmpRng = rng.duplicate(); 15519 tmpRng.collapse(false); 15520 endOffset = (tmpRng.move('character', offset) * -1) - startOffset; 15521 } 15522 } 15523 15524 // Wrap non block elements and text nodes 15525 node = rootNode.firstChild; 15526 rootNodeName = rootNode.nodeName.toLowerCase(); 15527 while (node) { 15528 // TODO: Break this up, too complex 15529 if (((node.nodeType === 3 || (node.nodeType == 1 && !blockElements[node.nodeName]))) && 15530 schema.isValidChild(rootNodeName, forcedRootBlock.toLowerCase())) { 15531 // Remove empty text nodes 15532 if (node.nodeType === 3 && node.nodeValue.length === 0) { 15533 tempNode = node; 15534 node = node.nextSibling; 15535 dom.remove(tempNode); 15536 continue; 15537 } 15538 15539 if (!rootBlockNode) { 15540 rootBlockNode = dom.create(forcedRootBlock, editor.settings.forced_root_block_attrs); 15541 node.parentNode.insertBefore(rootBlockNode, node); 15542 wrapped = true; 15543 } 15544 15545 tempNode = node; 15546 node = node.nextSibling; 15547 rootBlockNode.appendChild(tempNode); 15548 } else { 15549 rootBlockNode = null; 15550 node = node.nextSibling; 15551 } 15552 } 15553 15554 if (wrapped && restoreSelection) { 15555 if (rng.setStart) { 15556 rng.setStart(startContainer, startOffset); 15557 rng.setEnd(endContainer, endOffset); 15558 selection.setRng(rng); 15559 } else { 15560 // Only select if the previous selection was inside the document to prevent auto focus in quirks mode 15561 try { 15562 rng = editor.getDoc().body.createTextRange(); 15563 rng.moveToElementText(rootNode); 15564 rng.collapse(true); 15565 rng.moveStart('character', startOffset); 15566 15567 if (endOffset > 0) { 15568 rng.moveEnd('character', endOffset); 15569 } 15570 15571 rng.select(); 15572 } catch (ex) { 15573 // Ignore 15574 } 15575 } 15576 15577 editor.nodeChanged(); 15578 } 15579 } 15580 15581 // Force root blocks 15582 if (settings.forced_root_block) { 15583 editor.on('NodeChange', addRootBlocks); 15584 } 15585 }; 15586 }); 15587 15588 // Included from: js/tinymce/classes/EditorCommands.js 15589 15590 /** 15591 * EditorCommands.js 15592 * 15593 * Copyright, Moxiecode Systems AB 15594 * Released under LGPL License. 15595 * 15596 * License: http://www.tinymce.com/license 15597 * Contributing: http://www.tinymce.com/contributing 15598 */ 15599 15600 /** 15601 * This class enables you to add custom editor commands and it contains 15602 * overrides for native browser commands to address various bugs and issues. 15603 * 15604 * @class tinymce.EditorCommands 15605 */ 15606 define("tinymce/EditorCommands", [ 15607 "tinymce/html/Serializer", 15608 "tinymce/Env", 15609 "tinymce/util/Tools" 15610 ], function(Serializer, Env, Tools) { 15611 // Added for compression purposes 15612 var each = Tools.each, extend = Tools.extend; 15613 var map = Tools.map, inArray = Tools.inArray, explode = Tools.explode; 15614 var isGecko = Env.gecko, isIE = Env.ie; 15615 var TRUE = true, FALSE = false; 15616 15617 return function(editor) { 15618 var dom = editor.dom, 15619 selection = editor.selection, 15620 commands = {state: {}, exec: {}, value: {}}, 15621 settings = editor.settings, 15622 formatter = editor.formatter, 15623 bookmark; 15624 15625 /** 15626 * Executes the specified command. 15627 * 15628 * @method execCommand 15629 * @param {String} command Command to execute. 15630 * @param {Boolean} ui Optional user interface state. 15631 * @param {Object} value Optional value for command. 15632 * @return {Boolean} true/false if the command was found or not. 15633 */ 15634 function execCommand(command, ui, value) { 15635 var func; 15636 15637 command = command.toLowerCase(); 15638 if ((func = commands.exec[command])) { 15639 func(command, ui, value); 15640 return TRUE; 15641 } 15642 15643 return FALSE; 15644 } 15645 15646 /** 15647 * Queries the current state for a command for example if the current selection is "bold". 15648 * 15649 * @method queryCommandState 15650 * @param {String} command Command to check the state of. 15651 * @return {Boolean/Number} true/false if the selected contents is bold or not, -1 if it's not found. 15652 */ 15653 function queryCommandState(command) { 15654 var func; 15655 15656 command = command.toLowerCase(); 15657 if ((func = commands.state[command])) { 15658 return func(command); 15659 } 15660 15661 return -1; 15662 } 15663 15664 /** 15665 * Queries the command value for example the current fontsize. 15666 * 15667 * @method queryCommandValue 15668 * @param {String} command Command to check the value of. 15669 * @return {Object} Command value of false if it's not found. 15670 */ 15671 function queryCommandValue(command) { 15672 var func; 15673 15674 command = command.toLowerCase(); 15675 if ((func = commands.value[command])) { 15676 return func(command); 15677 } 15678 15679 return FALSE; 15680 } 15681 15682 /** 15683 * Adds commands to the command collection. 15684 * 15685 * @method addCommands 15686 * @param {Object} command_list Name/value collection with commands to add, the names can also be comma separated. 15687 * @param {String} type Optional type to add, defaults to exec. Can be value or state as well. 15688 */ 15689 function addCommands(command_list, type) { 15690 type = type || 'exec'; 15691 15692 each(command_list, function(callback, command) { 15693 each(command.toLowerCase().split(','), function(command) { 15694 commands[type][command] = callback; 15695 }); 15696 }); 15697 } 15698 15699 // Expose public methods 15700 extend(this, { 15701 execCommand: execCommand, 15702 queryCommandState: queryCommandState, 15703 queryCommandValue: queryCommandValue, 15704 addCommands: addCommands 15705 }); 15706 15707 // Private methods 15708 15709 function execNativeCommand(command, ui, value) { 15710 if (ui === undefined) { 15711 ui = FALSE; 15712 } 15713 15714 if (value === undefined) { 15715 value = null; 15716 } 15717 15718 return editor.getDoc().execCommand(command, ui, value); 15719 } 15720 15721 function isFormatMatch(name) { 15722 return formatter.match(name); 15723 } 15724 15725 function toggleFormat(name, value) { 15726 formatter.toggle(name, value ? {value: value} : undefined); 15727 editor.nodeChanged(); 15728 } 15729 15730 function storeSelection(type) { 15731 bookmark = selection.getBookmark(type); 15732 } 15733 15734 function restoreSelection() { 15735 selection.moveToBookmark(bookmark); 15736 } 15737 15738 // Add execCommand overrides 15739 addCommands({ 15740 // Ignore these, added for compatibility 15741 'mceResetDesignMode,mceBeginUndoLevel': function() {}, 15742 15743 // Add undo manager logic 15744 'mceEndUndoLevel,mceAddUndoLevel': function() { 15745 editor.undoManager.add(); 15746 }, 15747 15748 'Cut,Copy,Paste': function(command) { 15749 var doc = editor.getDoc(), failed; 15750 15751 // Try executing the native command 15752 try { 15753 execNativeCommand(command); 15754 } catch (ex) { 15755 // Command failed 15756 failed = TRUE; 15757 } 15758 15759 // Present alert message about clipboard access not being available 15760 if (failed || !doc.queryCommandSupported(command)) { 15761 var msg = editor.translate( 15762 "Your browser doesn't support direct access to the clipboard. " + 15763 "Please use the Ctrl+X/C/V keyboard shortcuts instead." 15764 ); 15765 15766 if (Env.mac) { 15767 msg = msg.replace(/Ctrl\+/g, '\u2318+'); 15768 } 15769 15770 editor.windowManager.alert(msg); 15771 } 15772 }, 15773 15774 // Override unlink command 15775 unlink: function() { 15776 if (selection.isCollapsed()) { 15777 var elm = selection.getNode(); 15778 if (elm.tagName == 'A') { 15779 editor.dom.remove(elm, true); 15780 } 15781 15782 return; 15783 } 15784 15785 formatter.remove("link"); 15786 }, 15787 15788 // Override justify commands to use the text formatter engine 15789 'JustifyLeft,JustifyCenter,JustifyRight,JustifyFull': function(command) { 15790 var align = command.substring(7); 15791 15792 if (align == 'full') { 15793 align = 'justify'; 15794 } 15795 15796 // Remove all other alignments first 15797 each('left,center,right,justify'.split(','), function(name) { 15798 if (align != name) { 15799 formatter.remove('align' + name); 15800 } 15801 }); 15802 15803 toggleFormat('align' + align); 15804 execCommand('mceRepaint'); 15805 }, 15806 15807 // Override list commands to fix WebKit bug 15808 'InsertUnorderedList,InsertOrderedList': function(command) { 15809 var listElm, listParent; 15810 15811 execNativeCommand(command); 15812 15813 // WebKit produces lists within block elements so we need to split them 15814 // we will replace the native list creation logic to custom logic later on 15815 // TODO: Remove this when the list creation logic is removed 15816 listElm = dom.getParent(selection.getNode(), 'ol,ul'); 15817 if (listElm) { 15818 listParent = listElm.parentNode; 15819 15820 // If list is within a text block then split that block 15821 if (/^(H[1-6]|P|ADDRESS|PRE)$/.test(listParent.nodeName)) { 15822 storeSelection(); 15823 dom.split(listParent, listElm); 15824 restoreSelection(); 15825 } 15826 } 15827 }, 15828 15829 // Override commands to use the text formatter engine 15830 'Bold,Italic,Underline,Strikethrough,Superscript,Subscript': function(command) { 15831 toggleFormat(command); 15832 }, 15833 15834 // Override commands to use the text formatter engine 15835 'ForeColor,HiliteColor,FontName': function(command, ui, value) { 15836 toggleFormat(command, value); 15837 }, 15838 15839 FontSize: function(command, ui, value) { 15840 var fontClasses, fontSizes; 15841 15842 // Convert font size 1-7 to styles 15843 if (value >= 1 && value <= 7) { 15844 fontSizes = explode(settings.font_size_style_values); 15845 fontClasses = explode(settings.font_size_classes); 15846 15847 if (fontClasses) { 15848 value = fontClasses[value - 1] || value; 15849 } else { 15850 value = fontSizes[value - 1] || value; 15851 } 15852 } 15853 15854 toggleFormat(command, value); 15855 }, 15856 15857 RemoveFormat: function(command) { 15858 formatter.remove(command); 15859 }, 15860 15861 mceBlockQuote: function() { 15862 toggleFormat('blockquote'); 15863 }, 15864 15865 FormatBlock: function(command, ui, value) { 15866 return toggleFormat(value || 'p'); 15867 }, 15868 15869 mceCleanup: function() { 15870 var bookmark = selection.getBookmark(); 15871 15872 editor.setContent(editor.getContent({cleanup: TRUE}), {cleanup: TRUE}); 15873 15874 selection.moveToBookmark(bookmark); 15875 }, 15876 15877 mceRemoveNode: function(command, ui, value) { 15878 var node = value || selection.getNode(); 15879 15880 // Make sure that the body node isn't removed 15881 if (node != editor.getBody()) { 15882 storeSelection(); 15883 editor.dom.remove(node, TRUE); 15884 restoreSelection(); 15885 } 15886 }, 15887 15888 mceSelectNodeDepth: function(command, ui, value) { 15889 var counter = 0; 15890 15891 dom.getParent(selection.getNode(), function(node) { 15892 if (node.nodeType == 1 && counter++ == value) { 15893 selection.select(node); 15894 return FALSE; 15895 } 15896 }, editor.getBody()); 15897 }, 15898 15899 mceSelectNode: function(command, ui, value) { 15900 selection.select(value); 15901 }, 15902 15903 mceInsertContent: function(command, ui, value) { 15904 var parser, serializer, parentNode, rootNode, fragment, args; 15905 var marker, rng, node, node2, bookmarkHtml; 15906 15907 function trimOrPaddLeftRight(html) { 15908 var rng, container, offset; 15909 15910 rng = selection.getRng(true); 15911 container = rng.startContainer; 15912 offset = rng.startOffset; 15913 15914 function hasSiblingText(siblingName) { 15915 return container[siblingName] && container[siblingName].nodeType == 3; 15916 } 15917 15918 if (container.nodeType == 3) { 15919 if (offset > 0) { 15920 html = html.replace(/^ /, ' '); 15921 } else if (!hasSiblingText('previousSibling')) { 15922 html = html.replace(/^ /, ' '); 15923 } 15924 15925 if (offset < container.length) { 15926 html = html.replace(/ (<br>|)$/, ' '); 15927 } else if (!hasSiblingText('nextSibling')) { 15928 html = html.replace(/( | )(<br>|)$/, ' '); 15929 } 15930 } 15931 15932 return html; 15933 } 15934 15935 // Check for whitespace before/after value 15936 if (/^ | $/.test(value)) { 15937 value = trimOrPaddLeftRight(value); 15938 } 15939 15940 // Setup parser and serializer 15941 parser = editor.parser; 15942 serializer = new Serializer({}, editor.schema); 15943 bookmarkHtml = '<span id="mce_marker" data-mce-type="bookmark">ÈB;</span>'; 15944 15945 // Run beforeSetContent handlers on the HTML to be inserted 15946 args = {content: value, format: 'html', selection: true}; 15947 editor.fire('BeforeSetContent', args); 15948 value = args.content; 15949 15950 // Add caret at end of contents if it's missing 15951 if (value.indexOf('{$caret}') == -1) { 15952 value += '{$caret}'; 15953 } 15954 15955 // Replace the caret marker with a span bookmark element 15956 value = value.replace(/\{\$caret\}/, bookmarkHtml); 15957 15958 // If selection is at <body>|<p></p> then move it into <body><p>|</p> 15959 rng = selection.getRng(); 15960 var caretElement = rng.startContainer || (rng.parentElement ? rng.parentElement() : null); 15961 var body = editor.getBody(); 15962 if (caretElement === body && selection.isCollapsed()) { 15963 if (dom.isBlock(body.firstChild) && dom.isEmpty(body.firstChild)) { 15964 rng = dom.createRng(); 15965 rng.setStart(body.firstChild, 0); 15966 rng.setEnd(body.firstChild, 0); 15967 selection.setRng(rng); 15968 } 15969 } 15970 15971 // Insert node maker where we will insert the new HTML and get it's parent 15972 if (!selection.isCollapsed()) { 15973 editor.getDoc().execCommand('Delete', false, null); 15974 } 15975 15976 parentNode = selection.getNode(); 15977 15978 // Parse the fragment within the context of the parent node 15979 var parserArgs = {context: parentNode.nodeName.toLowerCase()}; 15980 fragment = parser.parse(value, parserArgs); 15981 15982 // Move the caret to a more suitable location 15983 node = fragment.lastChild; 15984 if (node.attr('id') == 'mce_marker') { 15985 marker = node; 15986 15987 for (node = node.prev; node; node = node.walk(true)) { 15988 if (node.type == 3 || !dom.isBlock(node.name)) { 15989 node.parent.insert(marker, node, node.name === 'br'); 15990 break; 15991 } 15992 } 15993 } 15994 15995 // If parser says valid we can insert the contents into that parent 15996 if (!parserArgs.invalid) { 15997 value = serializer.serialize(fragment); 15998 15999 // Check if parent is empty or only has one BR element then set the innerHTML of that parent 16000 node = parentNode.firstChild; 16001 node2 = parentNode.lastChild; 16002 if (!node || (node === node2 && node.nodeName === 'BR')) { 16003 dom.setHTML(parentNode, value); 16004 } else { 16005 selection.setContent(value); 16006 } 16007 } else { 16008 // If the fragment was invalid within that context then we need 16009 // to parse and process the parent it's inserted into 16010 16011 // Insert bookmark node and get the parent 16012 selection.setContent(bookmarkHtml); 16013 parentNode = selection.getNode(); 16014 rootNode = editor.getBody(); 16015 16016 // Opera will return the document node when selection is in root 16017 if (parentNode.nodeType == 9) { 16018 parentNode = node = rootNode; 16019 } else { 16020 node = parentNode; 16021 } 16022 16023 // Find the ancestor just before the root element 16024 while (node !== rootNode) { 16025 parentNode = node; 16026 node = node.parentNode; 16027 } 16028 16029 // Get the outer/inner HTML depending on if we are in the root and parser and serialize that 16030 value = parentNode == rootNode ? rootNode.innerHTML : dom.getOuterHTML(parentNode); 16031 value = serializer.serialize( 16032 parser.parse( 16033 // Need to replace by using a function since $ in the contents would otherwise be a problem 16034 value.replace(/<span (id="mce_marker"|id=mce_marker).+?<\/span>/i, function() { 16035 return serializer.serialize(fragment); 16036 }) 16037 ) 16038 ); 16039 16040 // Set the inner/outer HTML depending on if we are in the root or not 16041 if (parentNode == rootNode) { 16042 dom.setHTML(rootNode, value); 16043 } else { 16044 dom.setOuterHTML(parentNode, value); 16045 } 16046 } 16047 16048 marker = dom.get('mce_marker'); 16049 selection.scrollIntoView(marker); 16050 16051 // Move selection before marker and remove it 16052 rng = dom.createRng(); 16053 16054 // If previous sibling is a text node set the selection to the end of that node 16055 node = marker.previousSibling; 16056 if (node && node.nodeType == 3) { 16057 rng.setStart(node, node.nodeValue.length); 16058 16059 // TODO: Why can't we normalize on IE 16060 if (!isIE) { 16061 node2 = marker.nextSibling; 16062 if (node2 && node2.nodeType == 3) { 16063 node.appendData(node2.data); 16064 node2.parentNode.removeChild(node2); 16065 } 16066 } 16067 } else { 16068 // If the previous sibling isn't a text node or doesn't exist set the selection before the marker node 16069 rng.setStartBefore(marker); 16070 rng.setEndBefore(marker); 16071 } 16072 16073 // Remove the marker node and set the new range 16074 dom.remove(marker); 16075 selection.setRng(rng); 16076 16077 // Dispatch after event and add any visual elements needed 16078 editor.fire('SetContent', args); 16079 editor.addVisual(); 16080 }, 16081 16082 mceInsertRawHTML: function(command, ui, value) { 16083 selection.setContent('tiny_mce_marker'); 16084 editor.setContent( 16085 editor.getContent().replace(/tiny_mce_marker/g, function() { 16086 return value; 16087 }) 16088 ); 16089 }, 16090 16091 mceToggleFormat: function(command, ui, value) { 16092 toggleFormat(value); 16093 }, 16094 16095 mceSetContent: function(command, ui, value) { 16096 editor.setContent(value); 16097 }, 16098 16099 'Indent,Outdent': function(command) { 16100 var intentValue, indentUnit, value; 16101 16102 // Setup indent level 16103 intentValue = settings.indentation; 16104 indentUnit = /[a-z%]+$/i.exec(intentValue); 16105 intentValue = parseInt(intentValue, 10); 16106 16107 if (!queryCommandState('InsertUnorderedList') && !queryCommandState('InsertOrderedList')) { 16108 // If forced_root_blocks is set to false we don't have a block to indent so lets create a div 16109 if (!settings.forced_root_block && !dom.getParent(selection.getNode(), dom.isBlock)) { 16110 formatter.apply('div'); 16111 } 16112 16113 each(selection.getSelectedBlocks(), function(element) { 16114 if (element.nodeName != "LI") { 16115 var indentStyleName = editor.getParam('indent_use_margin', false) ? 'margin' : 'padding'; 16116 16117 indentStyleName += dom.getStyle(element, 'direction', true) == 'rtl' ? 'Right' : 'Left'; 16118 16119 if (command == 'outdent') { 16120 value = Math.max(0, parseInt(element.style[indentStyleName] || 0, 10) - intentValue); 16121 dom.setStyle(element, indentStyleName, value ? value + indentUnit : ''); 16122 } else { 16123 value = (parseInt(element.style[indentStyleName] || 0, 10) + intentValue) + indentUnit; 16124 dom.setStyle(element, indentStyleName, value); 16125 } 16126 } 16127 }); 16128 } else { 16129 execNativeCommand(command); 16130 } 16131 }, 16132 16133 mceRepaint: function() { 16134 if (isGecko) { 16135 try { 16136 storeSelection(TRUE); 16137 16138 if (selection.getSel()) { 16139 selection.getSel().selectAllChildren(editor.getBody()); 16140 } 16141 16142 selection.collapse(TRUE); 16143 restoreSelection(); 16144 } catch (ex) { 16145 // Ignore 16146 } 16147 } 16148 }, 16149 16150 InsertHorizontalRule: function() { 16151 editor.execCommand('mceInsertContent', false, '<hr />'); 16152 }, 16153 16154 mceToggleVisualAid: function() { 16155 editor.hasVisual = !editor.hasVisual; 16156 editor.addVisual(); 16157 }, 16158 16159 mceReplaceContent: function(command, ui, value) { 16160 editor.execCommand('mceInsertContent', false, value.replace(/\{\$selection\}/g, selection.getContent({format: 'text'}))); 16161 }, 16162 16163 mceInsertLink: function(command, ui, value) { 16164 var anchor; 16165 16166 if (typeof(value) == 'string') { 16167 value = {href: value}; 16168 } 16169 16170 anchor = dom.getParent(selection.getNode(), 'a'); 16171 16172 // Spaces are never valid in URLs and it's a very common mistake for people to make so we fix it here. 16173 value.href = value.href.replace(' ', '%20'); 16174 16175 // Remove existing links if there could be child links or that the href isn't specified 16176 if (!anchor || !value.href) { 16177 formatter.remove('link'); 16178 } 16179 16180 // Apply new link to selection 16181 if (value.href) { 16182 formatter.apply('link', value, anchor); 16183 } 16184 }, 16185 16186 selectAll: function() { 16187 var root = dom.getRoot(), rng; 16188 16189 if (selection.getRng().setStart) { 16190 rng = dom.createRng(); 16191 rng.setStart(root, 0); 16192 rng.setEnd(root, root.childNodes.length); 16193 selection.setRng(rng); 16194 } else { 16195 // IE will render it's own root level block elements and sometimes 16196 // even put font elements in them when the user starts typing. So we need to 16197 // move the selection to a more suitable element from this: 16198 // <body>|<p></p></body> to this: <body><p>|</p></body> 16199 rng = selection.getRng(); 16200 if (!rng.item) { 16201 rng.moveToElementText(root); 16202 rng.select(); 16203 } 16204 } 16205 }, 16206 16207 "delete": function() { 16208 execNativeCommand("Delete"); 16209 16210 // Check if body is empty after the delete call if so then set the contents 16211 // to an empty string and move the caret to any block produced by that operation 16212 // this fixes the issue with root blocks not being properly produced after a delete call on IE 16213 var body = editor.getBody(); 16214 16215 if (dom.isEmpty(body)) { 16216 editor.setContent(''); 16217 16218 if (body.firstChild && dom.isBlock(body.firstChild)) { 16219 editor.selection.setCursorLocation(body.firstChild, 0); 16220 } else { 16221 editor.selection.setCursorLocation(body, 0); 16222 } 16223 } 16224 }, 16225 16226 mceNewDocument: function() { 16227 editor.setContent(''); 16228 } 16229 }); 16230 16231 // Add queryCommandState overrides 16232 addCommands({ 16233 // Override justify commands 16234 'JustifyLeft,JustifyCenter,JustifyRight,JustifyFull': function(command) { 16235 var name = 'align' + command.substring(7); 16236 var nodes = selection.isCollapsed() ? [dom.getParent(selection.getNode(), dom.isBlock)] : selection.getSelectedBlocks(); 16237 var matches = map(nodes, function(node) { 16238 return !!formatter.matchNode(node, name); 16239 }); 16240 return inArray(matches, TRUE) !== -1; 16241 }, 16242 16243 'Bold,Italic,Underline,Strikethrough,Superscript,Subscript': function(command) { 16244 return isFormatMatch(command); 16245 }, 16246 16247 mceBlockQuote: function() { 16248 return isFormatMatch('blockquote'); 16249 }, 16250 16251 Outdent: function() { 16252 var node; 16253 16254 if (settings.inline_styles) { 16255 if ((node = dom.getParent(selection.getStart(), dom.isBlock)) && parseInt(node.style.paddingLeft, 10) > 0) { 16256 return TRUE; 16257 } 16258 16259 if ((node = dom.getParent(selection.getEnd(), dom.isBlock)) && parseInt(node.style.paddingLeft, 10) > 0) { 16260 return TRUE; 16261 } 16262 } 16263 16264 return ( 16265 queryCommandState('InsertUnorderedList') || 16266 queryCommandState('InsertOrderedList') || 16267 (!settings.inline_styles && !!dom.getParent(selection.getNode(), 'BLOCKQUOTE')) 16268 ); 16269 }, 16270 16271 'InsertUnorderedList,InsertOrderedList': function(command) { 16272 var list = dom.getParent(selection.getNode(), 'ul,ol'); 16273 16274 return list && 16275 ( 16276 command === 'insertunorderedlist' && list.tagName === 'UL' || 16277 command === 'insertorderedlist' && list.tagName === 'OL' 16278 ); 16279 } 16280 }, 'state'); 16281 16282 // Add queryCommandValue overrides 16283 addCommands({ 16284 'FontSize,FontName': function(command) { 16285 var value = 0, parent; 16286 16287 if ((parent = dom.getParent(selection.getNode(), 'span'))) { 16288 if (command == 'fontsize') { 16289 value = parent.style.fontSize; 16290 } else { 16291 value = parent.style.fontFamily.replace(/, /g, ',').replace(/[\'\"]/g, '').toLowerCase(); 16292 } 16293 } 16294 16295 return value; 16296 } 16297 }, 'value'); 16298 16299 // Add undo manager logic 16300 addCommands({ 16301 Undo: function() { 16302 editor.undoManager.undo(); 16303 }, 16304 16305 Redo: function() { 16306 editor.undoManager.redo(); 16307 } 16308 }); 16309 }; 16310 }); 16311 16312 // Included from: js/tinymce/classes/util/URI.js 16313 16314 /** 16315 * URI.js 16316 * 16317 * Copyright, Moxiecode Systems AB 16318 * Released under LGPL License. 16319 * 16320 * License: http://www.tinymce.com/license 16321 * Contributing: http://www.tinymce.com/contributing 16322 */ 16323 16324 /** 16325 * This class handles parsing, modification and serialization of URI/URL strings. 16326 * @class tinymce.util.URI 16327 */ 16328 define("tinymce/util/URI", [ 16329 "tinymce/util/Tools" 16330 ], function(Tools) { 16331 var each = Tools.each, trim = Tools.trim, 16332 DEFAULT_PORTS = { 16333 'ftp': 21, 16334 'http': 80, 16335 'https': 443, 16336 'mailto': 25 16337 }; 16338 16339 /** 16340 * Constructs a new URI instance. 16341 * 16342 * @constructor 16343 * @method URI 16344 * @param {String} url URI string to parse. 16345 * @param {Object} settings Optional settings object. 16346 */ 16347 function URI(url, settings) { 16348 var self = this, baseUri, base_url; 16349 16350 // Trim whitespace 16351 url = trim(url); 16352 16353 // Default settings 16354 settings = self.settings = settings || {}; 16355 16356 // Strange app protocol that isn't http/https or local anchor 16357 // For example: mailto,skype,tel etc. 16358 if (/^([\w\-]+):([^\/]{2})/i.test(url) || /^\s*#/.test(url)) { 16359 self.source = url; 16360 return; 16361 } 16362 16363 var isProtocolRelative = url.indexOf('//') === 0; 16364 16365 // Absolute path with no host, fake host and protocol 16366 if (url.indexOf('/') === 0 && !isProtocolRelative) { 16367 url = (settings.base_uri ? settings.base_uri.protocol || 'http' : 'http') + '://mce_host' + url; 16368 } 16369 16370 // Relative path http:// or protocol relative //path 16371 if (!/^[\w\-]*:?\/\//.test(url)) { 16372 base_url = settings.base_uri ? settings.base_uri.path : new URI(location.href).directory; 16373 if (settings.base_uri.protocol === "") { 16374 url = '//mce_host' + self.toAbsPath(base_url, url); 16375 } else { 16376 url = ((settings.base_uri && settings.base_uri.protocol) || 'http') + '://mce_host' + self.toAbsPath(base_url, url); 16377 } 16378 } 16379 16380 // Parse URL (Credits goes to Steave, http://blog.stevenlevithan.com/archives/parseuri) 16381 url = url.replace(/@@/g, '(mce_at)'); // Zope 3 workaround, they use @@something 16382 16383 /*jshint maxlen: 255 */ 16384 /*eslint max-len: 0 */ 16385 url = /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@\/]*):?([^:@\/]*))?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/.exec(url); 16386 16387 each(["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"], function(v, i) { 16388 var part = url[i]; 16389 16390 // Zope 3 workaround, they use @@something 16391 if (part) { 16392 part = part.replace(/\(mce_at\)/g, '@@'); 16393 } 16394 16395 self[v] = part; 16396 }); 16397 16398 baseUri = settings.base_uri; 16399 if (baseUri) { 16400 if (!self.protocol) { 16401 self.protocol = baseUri.protocol; 16402 } 16403 16404 if (!self.userInfo) { 16405 self.userInfo = baseUri.userInfo; 16406 } 16407 16408 if (!self.port && self.host === 'mce_host') { 16409 self.port = baseUri.port; 16410 } 16411 16412 if (!self.host || self.host === 'mce_host') { 16413 self.host = baseUri.host; 16414 } 16415 16416 self.source = ''; 16417 } 16418 16419 if (isProtocolRelative) { 16420 self.protocol = ''; 16421 } 16422 16423 //t.path = t.path || '/'; 16424 } 16425 16426 URI.prototype = { 16427 /** 16428 * Sets the internal path part of the URI. 16429 * 16430 * @method setPath 16431 * @param {string} path Path string to set. 16432 */ 16433 setPath: function(path) { 16434 var self = this; 16435 16436 path = /^(.*?)\/?(\w+)?$/.exec(path); 16437 16438 // Update path parts 16439 self.path = path[0]; 16440 self.directory = path[1]; 16441 self.file = path[2]; 16442 16443 // Rebuild source 16444 self.source = ''; 16445 self.getURI(); 16446 }, 16447 16448 /** 16449 * Converts the specified URI into a relative URI based on the current URI instance location. 16450 * 16451 * @method toRelative 16452 * @param {String} uri URI to convert into a relative path/URI. 16453 * @return {String} Relative URI from the point specified in the current URI instance. 16454 * @example 16455 * // Converts an absolute URL to an relative URL url will be somedir/somefile.htm 16456 * var url = new tinymce.util.URI('http://www.site.com/dir/').toRelative('http://www.site.com/dir/somedir/somefile.htm'); 16457 */ 16458 toRelative: function(uri) { 16459 var self = this, output; 16460 16461 if (uri === "./") { 16462 return uri; 16463 } 16464 16465 uri = new URI(uri, {base_uri: self}); 16466 16467 // Not on same domain/port or protocol 16468 if ((uri.host != 'mce_host' && self.host != uri.host && uri.host) || self.port != uri.port || 16469 (self.protocol != uri.protocol && uri.protocol !== "")) { 16470 return uri.getURI(); 16471 } 16472 16473 var tu = self.getURI(), uu = uri.getURI(); 16474 16475 // Allow usage of the base_uri when relative_urls = true 16476 if (tu == uu || (tu.charAt(tu.length - 1) == "/" && tu.substr(0, tu.length - 1) == uu)) { 16477 return tu; 16478 } 16479 16480 output = self.toRelPath(self.path, uri.path); 16481 16482 // Add query 16483 if (uri.query) { 16484 output += '?' + uri.query; 16485 } 16486 16487 // Add anchor 16488 if (uri.anchor) { 16489 output += '#' + uri.anchor; 16490 } 16491 16492 return output; 16493 }, 16494 16495 /** 16496 * Converts the specified URI into a absolute URI based on the current URI instance location. 16497 * 16498 * @method toAbsolute 16499 * @param {String} uri URI to convert into a relative path/URI. 16500 * @param {Boolean} noHost No host and protocol prefix. 16501 * @return {String} Absolute URI from the point specified in the current URI instance. 16502 * @example 16503 * // Converts an relative URL to an absolute URL url will be http://www.site.com/dir/somedir/somefile.htm 16504 * var url = new tinymce.util.URI('http://www.site.com/dir/').toAbsolute('somedir/somefile.htm'); 16505 */ 16506 toAbsolute: function(uri, noHost) { 16507 uri = new URI(uri, {base_uri: this}); 16508 16509 return uri.getURI(noHost && this.isSameOrigin(uri)); 16510 }, 16511 16512 /** 16513 * Determine whether the given URI has the same origin as this URI. Based on RFC-6454. 16514 * Supports default ports for protocols listed in DEFAULT_PORTS. Unsupported protocols will fail safe: they 16515 * won't match, if the port specifications differ. 16516 * 16517 * @method isSameOrigin 16518 * @param {tinymce.util.URI} uri Uri instance to compare. 16519 * @returns {Boolean} True if the origins are the same. 16520 */ 16521 isSameOrigin: function(uri) { 16522 if (this.host == uri.host && this.protocol == uri.protocol){ 16523 if (this.port == uri.port) { 16524 return true; 16525 } 16526 16527 var defaultPort = DEFAULT_PORTS[this.protocol]; 16528 if (defaultPort && ((this.port || defaultPort) == (uri.port || defaultPort))) { 16529 return true; 16530 } 16531 } 16532 16533 return false; 16534 }, 16535 16536 /** 16537 * Converts a absolute path into a relative path. 16538 * 16539 * @method toRelPath 16540 * @param {String} base Base point to convert the path from. 16541 * @param {String} path Absolute path to convert into a relative path. 16542 */ 16543 toRelPath: function(base, path) { 16544 var items, breakPoint = 0, out = '', i, l; 16545 16546 // Split the paths 16547 base = base.substring(0, base.lastIndexOf('/')); 16548 base = base.split('/'); 16549 items = path.split('/'); 16550 16551 if (base.length >= items.length) { 16552 for (i = 0, l = base.length; i < l; i++) { 16553 if (i >= items.length || base[i] != items[i]) { 16554 breakPoint = i + 1; 16555 break; 16556 } 16557 } 16558 } 16559 16560 if (base.length < items.length) { 16561 for (i = 0, l = items.length; i < l; i++) { 16562 if (i >= base.length || base[i] != items[i]) { 16563 breakPoint = i + 1; 16564 break; 16565 } 16566 } 16567 } 16568 16569 if (breakPoint === 1) { 16570 return path; 16571 } 16572 16573 for (i = 0, l = base.length - (breakPoint - 1); i < l; i++) { 16574 out += "../"; 16575 } 16576 16577 for (i = breakPoint - 1, l = items.length; i < l; i++) { 16578 if (i != breakPoint - 1) { 16579 out += "/" + items[i]; 16580 } else { 16581 out += items[i]; 16582 } 16583 } 16584 16585 return out; 16586 }, 16587 16588 /** 16589 * Converts a relative path into a absolute path. 16590 * 16591 * @method toAbsPath 16592 * @param {String} base Base point to convert the path from. 16593 * @param {String} path Relative path to convert into an absolute path. 16594 */ 16595 toAbsPath: function(base, path) { 16596 var i, nb = 0, o = [], tr, outPath; 16597 16598 // Split paths 16599 tr = /\/$/.test(path) ? '/' : ''; 16600 base = base.split('/'); 16601 path = path.split('/'); 16602 16603 // Remove empty chunks 16604 each(base, function(k) { 16605 if (k) { 16606 o.push(k); 16607 } 16608 }); 16609 16610 base = o; 16611 16612 // Merge relURLParts chunks 16613 for (i = path.length - 1, o = []; i >= 0; i--) { 16614 // Ignore empty or . 16615 if (path[i].length === 0 || path[i] === ".") { 16616 continue; 16617 } 16618 16619 // Is parent 16620 if (path[i] === '..') { 16621 nb++; 16622 continue; 16623 } 16624 16625 // Move up 16626 if (nb > 0) { 16627 nb--; 16628 continue; 16629 } 16630 16631 o.push(path[i]); 16632 } 16633 16634 i = base.length - nb; 16635 16636 // If /a/b/c or / 16637 if (i <= 0) { 16638 outPath = o.reverse().join('/'); 16639 } else { 16640 outPath = base.slice(0, i).join('/') + '/' + o.reverse().join('/'); 16641 } 16642 16643 // Add front / if it's needed 16644 if (outPath.indexOf('/') !== 0) { 16645 outPath = '/' + outPath; 16646 } 16647 16648 // Add traling / if it's needed 16649 if (tr && outPath.lastIndexOf('/') !== outPath.length - 1) { 16650 outPath += tr; 16651 } 16652 16653 return outPath; 16654 }, 16655 16656 /** 16657 * Returns the full URI of the internal structure. 16658 * 16659 * @method getURI 16660 * @param {Boolean} noProtoHost Optional no host and protocol part. Defaults to false. 16661 */ 16662 getURI: function(noProtoHost) { 16663 var s, self = this; 16664 16665 // Rebuild source 16666 if (!self.source || noProtoHost) { 16667 s = ''; 16668 16669 if (!noProtoHost) { 16670 if (self.protocol) { 16671 s += self.protocol + '://'; 16672 } else { 16673 s += '//'; 16674 } 16675 16676 if (self.userInfo) { 16677 s += self.userInfo + '@'; 16678 } 16679 16680 if (self.host) { 16681 s += self.host; 16682 } 16683 16684 if (self.port) { 16685 s += ':' + self.port; 16686 } 16687 } 16688 16689 if (self.path) { 16690 s += self.path; 16691 } 16692 16693 if (self.query) { 16694 s += '?' + self.query; 16695 } 16696 16697 if (self.anchor) { 16698 s += '#' + self.anchor; 16699 } 16700 16701 self.source = s; 16702 } 16703 16704 return self.source; 16705 } 16706 }; 16707 16708 return URI; 16709 }); 16710 16711 // Included from: js/tinymce/classes/util/Class.js 16712 16713 /** 16714 * Class.js 16715 * 16716 * Copyright 2003-2012, Moxiecode Systems AB, All rights reserved. 16717 */ 16718 16719 /** 16720 * This utilitiy class is used for easier inheritage. 16721 * 16722 * Features: 16723 * * Exposed super functions: this._super(); 16724 * * Mixins 16725 * * Dummy functions 16726 * * Property functions: var value = object.value(); and object.value(newValue); 16727 * * Static functions 16728 * * Defaults settings 16729 */ 16730 define("tinymce/util/Class", [ 16731 "tinymce/util/Tools" 16732 ], function(Tools) { 16733 var each = Tools.each, extend = Tools.extend; 16734 16735 var extendClass, initializing; 16736 16737 function Class() { 16738 } 16739 16740 // Provides classical inheritance, based on code made by John Resig 16741 Class.extend = extendClass = function(prop) { 16742 var self = this, _super = self.prototype, prototype, name, member; 16743 16744 // The dummy class constructor 16745 function Class() { 16746 var i, mixins, mixin, self = this; 16747 16748 // All construction is actually done in the init method 16749 if (!initializing) { 16750 // Run class constuctor 16751 if (self.init) { 16752 self.init.apply(self, arguments); 16753 } 16754 16755 // Run mixin constructors 16756 mixins = self.Mixins; 16757 if (mixins) { 16758 i = mixins.length; 16759 while (i--) { 16760 mixin = mixins[i]; 16761 if (mixin.init) { 16762 mixin.init.apply(self, arguments); 16763 } 16764 } 16765 } 16766 } 16767 } 16768 16769 // Dummy function, needs to be extended in order to provide functionality 16770 function dummy() { 16771 return this; 16772 } 16773 16774 // Creates a overloaded method for the class 16775 // this enables you to use this._super(); to call the super function 16776 function createMethod(name, fn) { 16777 return function(){ 16778 var self = this, tmp = self._super, ret; 16779 16780 self._super = _super[name]; 16781 ret = fn.apply(self, arguments); 16782 self._super = tmp; 16783 16784 return ret; 16785 }; 16786 } 16787 16788 // Instantiate a base class (but only create the instance, 16789 // don't run the init constructor) 16790 initializing = true; 16791 16792 /*eslint new-cap:0 */ 16793 prototype = new self(); 16794 initializing = false; 16795 16796 // Add mixins 16797 if (prop.Mixins) { 16798 each(prop.Mixins, function(mixin) { 16799 mixin = mixin; 16800 16801 for (var name in mixin) { 16802 if (name !== "init") { 16803 prop[name] = mixin[name]; 16804 } 16805 } 16806 }); 16807 16808 if (_super.Mixins) { 16809 prop.Mixins = _super.Mixins.concat(prop.Mixins); 16810 } 16811 } 16812 16813 // Generate dummy methods 16814 if (prop.Methods) { 16815 each(prop.Methods.split(','), function(name) { 16816 prop[name] = dummy; 16817 }); 16818 } 16819 16820 // Generate property methods 16821 if (prop.Properties) { 16822 each(prop.Properties.split(','), function(name) { 16823 var fieldName = '_' + name; 16824 16825 prop[name] = function(value) { 16826 var self = this, undef; 16827 16828 // Set value 16829 if (value !== undef) { 16830 self[fieldName] = value; 16831 16832 return self; 16833 } 16834 16835 // Get value 16836 return self[fieldName]; 16837 }; 16838 }); 16839 } 16840 16841 // Static functions 16842 if (prop.Statics) { 16843 each(prop.Statics, function(func, name) { 16844 Class[name] = func; 16845 }); 16846 } 16847 16848 // Default settings 16849 if (prop.Defaults && _super.Defaults) { 16850 prop.Defaults = extend({}, _super.Defaults, prop.Defaults); 16851 } 16852 16853 // Copy the properties over onto the new prototype 16854 for (name in prop) { 16855 member = prop[name]; 16856 16857 if (typeof member == "function" && _super[name]) { 16858 prototype[name] = createMethod(name, member); 16859 } else { 16860 prototype[name] = member; 16861 } 16862 } 16863 16864 // Populate our constructed prototype object 16865 Class.prototype = prototype; 16866 16867 // Enforce the constructor to be what we expect 16868 Class.constructor = Class; 16869 16870 // And make this class extendible 16871 Class.extend = extendClass; 16872 16873 return Class; 16874 }; 16875 16876 return Class; 16877 }); 16878 16879 // Included from: js/tinymce/classes/util/EventDispatcher.js 16880 16881 /** 16882 * EventDispatcher.js 16883 * 16884 * Copyright, Moxiecode Systems AB 16885 * Released under LGPL License. 16886 * 16887 * License: http://www.tinymce.com/license 16888 * Contributing: http://www.tinymce.com/contributing 16889 */ 16890 16891 /** 16892 * This class lets you add/remove and fire events by name on the specified scope. This makes 16893 * it easy to add event listener logic to any class. 16894 * 16895 * @class tinymce.util.EventDispatcher 16896 * @example 16897 * var eventDispatcher = new EventDispatcher(); 16898 * 16899 * eventDispatcher.on('click', function() {console.log('data');}); 16900 * eventDispatcher.fire('click', {data: 123}); 16901 */ 16902 define("tinymce/util/EventDispatcher", [ 16903 "tinymce/util/Tools" 16904 ], function(Tools) { 16905 var nativeEvents = Tools.makeMap( 16906 "focus blur focusin focusout click dblclick mousedown mouseup mousemove mouseover beforepaste paste cut copy selectionchange " + 16907 "mouseout mouseenter mouseleave wheel keydown keypress keyup input contextmenu dragstart dragend dragover " + 16908 "draggesture dragdrop drop drag submit", 16909 ' ' 16910 ); 16911 16912 function Dispatcher(settings) { 16913 var self = this, scope, bindings = {}, toggleEvent; 16914 16915 function returnFalse() { 16916 return false; 16917 } 16918 16919 function returnTrue() { 16920 return true; 16921 } 16922 16923 settings = settings || {}; 16924 scope = settings.scope || self; 16925 toggleEvent = settings.toggleEvent || returnFalse; 16926 16927 /** 16928 * Fires the specified event by name. 16929 * 16930 * @method fire 16931 * @param {String} name Name of the event to fire. 16932 * @param {Object?} args Event arguments. 16933 * @return {Object} Event args instance passed in. 16934 * @example 16935 * instance.fire('event', {...}); 16936 */ 16937 function fire(name, args) { 16938 var handlers, i, l, callback; 16939 16940 name = name.toLowerCase(); 16941 args = args || {}; 16942 args.type = name; 16943 16944 // Setup target is there isn't one 16945 if (!args.target) { 16946 args.target = scope; 16947 } 16948 16949 // Add event delegation methods if they are missing 16950 if (!args.preventDefault) { 16951 // Add preventDefault method 16952 args.preventDefault = function() { 16953 args.isDefaultPrevented = returnTrue; 16954 }; 16955 16956 // Add stopPropagation 16957 args.stopPropagation = function() { 16958 args.isPropagationStopped = returnTrue; 16959 }; 16960 16961 // Add stopImmediatePropagation 16962 args.stopImmediatePropagation = function() { 16963 args.isImmediatePropagationStopped = returnTrue; 16964 }; 16965 16966 // Add event delegation states 16967 args.isDefaultPrevented = returnFalse; 16968 args.isPropagationStopped = returnFalse; 16969 args.isImmediatePropagationStopped = returnFalse; 16970 } 16971 16972 if (settings.beforeFire) { 16973 settings.beforeFire(args); 16974 } 16975 16976 handlers = bindings[name]; 16977 if (handlers) { 16978 for (i = 0, l = handlers.length; i < l; i++) { 16979 handlers[i] = callback = handlers[i]; 16980 16981 // Stop immediate propagation if needed 16982 if (args.isImmediatePropagationStopped()) { 16983 args.stopPropagation(); 16984 return args; 16985 } 16986 16987 // If callback returns false then prevent default and stop all propagation 16988 if (callback.call(scope, args) === false) { 16989 args.preventDefault(); 16990 return args; 16991 } 16992 } 16993 } 16994 16995 return args; 16996 } 16997 16998 /** 16999 * Binds an event listener to a specific event by name. 17000 * 17001 * @method on 17002 * @param {String} name Event name or space separated list of events to bind. 17003 * @param {callback} callback Callback to be executed when the event occurs. 17004 * @param {Boolean} first Optional flag if the event should be prepended. Use this with care. 17005 * @return {Object} Current class instance. 17006 * @example 17007 * instance.on('event', function(e) { 17008 * // Callback logic 17009 * }); 17010 */ 17011 function on(name, callback, prepend) { 17012 var handlers, names, i; 17013 17014 if (callback === false) { 17015 callback = returnFalse; 17016 } 17017 17018 if (callback) { 17019 names = name.toLowerCase().split(' '); 17020 i = names.length; 17021 while (i--) { 17022 name = names[i]; 17023 handlers = bindings[name]; 17024 if (!handlers) { 17025 handlers = bindings[name] = []; 17026 toggleEvent(name, true); 17027 } 17028 17029 if (prepend) { 17030 handlers.unshift(callback); 17031 } else { 17032 handlers.push(callback); 17033 } 17034 } 17035 } 17036 17037 return self; 17038 } 17039 17040 /** 17041 * Unbinds an event listener to a specific event by name. 17042 * 17043 * @method off 17044 * @param {String?} name Name of the event to unbind. 17045 * @param {callback?} callback Callback to unbind. 17046 * @return {Object} Current class instance. 17047 * @example 17048 * // Unbind specific callback 17049 * instance.off('event', handler); 17050 * 17051 * // Unbind all listeners by name 17052 * instance.off('event'); 17053 * 17054 * // Unbind all events 17055 * instance.off(); 17056 */ 17057 function off(name, callback) { 17058 var i, handlers, bindingName, names, hi; 17059 17060 if (name) { 17061 names = name.toLowerCase().split(' '); 17062 i = names.length; 17063 while (i--) { 17064 name = names[i]; 17065 handlers = bindings[name]; 17066 17067 // Unbind all handlers 17068 if (!name) { 17069 for (bindingName in bindings) { 17070 toggleEvent(bindingName, false); 17071 delete bindings[bindingName]; 17072 } 17073 17074 return self; 17075 } 17076 17077 if (handlers) { 17078 // Unbind all by name 17079 if (!callback) { 17080 handlers.length = 0; 17081 } else { 17082 // Unbind specific ones 17083 hi = handlers.length; 17084 while (hi--) { 17085 if (handlers[hi] === callback) { 17086 handlers.splice(hi, 1); 17087 } 17088 } 17089 } 17090 17091 if (!handlers.length) { 17092 toggleEvent(name, false); 17093 delete bindings[name]; 17094 } 17095 } 17096 } 17097 } else { 17098 for (name in bindings) { 17099 toggleEvent(name, false); 17100 } 17101 17102 bindings = {}; 17103 } 17104 17105 return self; 17106 } 17107 17108 /** 17109 * Returns true/false if the dispatcher has a event of the specified name. 17110 * 17111 * @method has 17112 * @param {String} name Name of the event to check for. 17113 * @return {Boolean} true/false if the event exists or not. 17114 */ 17115 function has(name) { 17116 name = name.toLowerCase(); 17117 return !(!bindings[name] || bindings[name].length === 0); 17118 } 17119 17120 // Expose 17121 self.fire = fire; 17122 self.on = on; 17123 self.off = off; 17124 self.has = has; 17125 } 17126 17127 /** 17128 * Returns true/false if the specified event name is a native browser event or not. 17129 * 17130 * @method isNative 17131 * @param {String} name Name to check if it's native. 17132 * @return {Boolean} true/false if the event is native or not. 17133 * @static 17134 */ 17135 Dispatcher.isNative = function(name) { 17136 return !!nativeEvents[name.toLowerCase()]; 17137 }; 17138 17139 return Dispatcher; 17140 }); 17141 17142 // Included from: js/tinymce/classes/ui/Selector.js 17143 17144 /** 17145 * Selector.js 17146 * 17147 * Copyright, Moxiecode Systems AB 17148 * Released under LGPL License. 17149 * 17150 * License: http://www.tinymce.com/license 17151 * Contributing: http://www.tinymce.com/contributing 17152 */ 17153 17154 /*eslint no-nested-ternary:0 */ 17155 17156 /** 17157 * Selector engine, enables you to select controls by using CSS like expressions. 17158 * We currently only support basic CSS expressions to reduce the size of the core 17159 * and the ones we support should be enough for most cases. 17160 * 17161 * @example 17162 * Supported expressions: 17163 * element 17164 * element#name 17165 * element.class 17166 * element[attr] 17167 * element[attr*=value] 17168 * element[attr~=value] 17169 * element[attr!=value] 17170 * element[attr^=value] 17171 * element[attr$=value] 17172 * element:<state> 17173 * element:not(<expression>) 17174 * element:first 17175 * element:last 17176 * element:odd 17177 * element:even 17178 * element element 17179 * element > element 17180 * 17181 * @class tinymce.ui.Selector 17182 */ 17183 define("tinymce/ui/Selector", [ 17184 "tinymce/util/Class" 17185 ], function(Class) { 17186 "use strict"; 17187 17188 /** 17189 * Produces an array with a unique set of objects. It will not compare the values 17190 * but the references of the objects. 17191 * 17192 * @private 17193 * @method unqiue 17194 * @param {Array} array Array to make into an array with unique items. 17195 * @return {Array} Array with unique items. 17196 */ 17197 function unique(array) { 17198 var uniqueItems = [], i = array.length, item; 17199 17200 while (i--) { 17201 item = array[i]; 17202 17203 if (!item.__checked) { 17204 uniqueItems.push(item); 17205 item.__checked = 1; 17206 } 17207 } 17208 17209 i = uniqueItems.length; 17210 while (i--) { 17211 delete uniqueItems[i].__checked; 17212 } 17213 17214 return uniqueItems; 17215 } 17216 17217 var expression = /^([\w\\*]+)?(?:#([\w\\]+))?(?:\.([\w\\\.]+))?(?:\[\@?([\w\\]+)([\^\$\*!~]?=)([\w\\]+)\])?(?:\:(.+))?/i; 17218 17219 /*jshint maxlen:255 */ 17220 /*eslint max-len:0 */ 17221 var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g, 17222 whiteSpace = /^\s*|\s*$/g, 17223 Collection; 17224 17225 var Selector = Class.extend({ 17226 /** 17227 * Constructs a new Selector instance. 17228 * 17229 * @constructor 17230 * @method init 17231 * @param {String} selector CSS like selector expression. 17232 */ 17233 init: function(selector) { 17234 var match = this.match; 17235 17236 function compileNameFilter(name) { 17237 if (name) { 17238 name = name.toLowerCase(); 17239 17240 return function(item) { 17241 return name === '*' || item.type === name; 17242 }; 17243 } 17244 } 17245 17246 function compileIdFilter(id) { 17247 if (id) { 17248 return function(item) { 17249 return item._name === id; 17250 }; 17251 } 17252 } 17253 17254 function compileClassesFilter(classes) { 17255 if (classes) { 17256 classes = classes.split('.'); 17257 17258 return function(item) { 17259 var i = classes.length; 17260 17261 while (i--) { 17262 if (!item.hasClass(classes[i])) { 17263 return false; 17264 } 17265 } 17266 17267 return true; 17268 }; 17269 } 17270 } 17271 17272 function compileAttrFilter(name, cmp, check) { 17273 if (name) { 17274 return function(item) { 17275 var value = item[name] ? item[name]() : ''; 17276 17277 return !cmp ? !!check : 17278 cmp === "=" ? value === check : 17279 cmp === "*=" ? value.indexOf(check) >= 0 : 17280 cmp === "~=" ? (" " + value + " ").indexOf(" " + check + " ") >= 0 : 17281 cmp === "!=" ? value != check : 17282 cmp === "^=" ? value.indexOf(check) === 0 : 17283 cmp === "$=" ? value.substr(value.length - check.length) === check : 17284 false; 17285 }; 17286 } 17287 } 17288 17289 function compilePsuedoFilter(name) { 17290 var notSelectors; 17291 17292 if (name) { 17293 name = /(?:not\((.+)\))|(.+)/i.exec(name); 17294 17295 if (!name[1]) { 17296 name = name[2]; 17297 17298 return function(item, index, length) { 17299 return name === 'first' ? index === 0 : 17300 name === 'last' ? index === length - 1 : 17301 name === 'even' ? index % 2 === 0 : 17302 name === 'odd' ? index % 2 === 1 : 17303 item[name] ? item[name]() : 17304 false; 17305 }; 17306 } else { 17307 // Compile not expression 17308 notSelectors = parseChunks(name[1], []); 17309 17310 return function(item) { 17311 return !match(item, notSelectors); 17312 }; 17313 } 17314 } 17315 } 17316 17317 function compile(selector, filters, direct) { 17318 var parts; 17319 17320 function add(filter) { 17321 if (filter) { 17322 filters.push(filter); 17323 } 17324 } 17325 17326 // Parse expression into parts 17327 parts = expression.exec(selector.replace(whiteSpace, '')); 17328 17329 add(compileNameFilter(parts[1])); 17330 add(compileIdFilter(parts[2])); 17331 add(compileClassesFilter(parts[3])); 17332 add(compileAttrFilter(parts[4], parts[5], parts[6])); 17333 add(compilePsuedoFilter(parts[7])); 17334 17335 // Mark the filter with psuedo for performance 17336 filters.psuedo = !!parts[7]; 17337 filters.direct = direct; 17338 17339 return filters; 17340 } 17341 17342 // Parser logic based on Sizzle by John Resig 17343 function parseChunks(selector, selectors) { 17344 var parts = [], extra, matches, i; 17345 17346 do { 17347 chunker.exec(""); 17348 matches = chunker.exec(selector); 17349 17350 if (matches) { 17351 selector = matches[3]; 17352 parts.push(matches[1]); 17353 17354 if (matches[2]) { 17355 extra = matches[3]; 17356 break; 17357 } 17358 } 17359 } while (matches); 17360 17361 if (extra) { 17362 parseChunks(extra, selectors); 17363 } 17364 17365 selector = []; 17366 for (i = 0; i < parts.length; i++) { 17367 if (parts[i] != '>') { 17368 selector.push(compile(parts[i], [], parts[i - 1] === '>')); 17369 } 17370 } 17371 17372 selectors.push(selector); 17373 17374 return selectors; 17375 } 17376 17377 this._selectors = parseChunks(selector, []); 17378 }, 17379 17380 /** 17381 * Returns true/false if the selector matches the specified control. 17382 * 17383 * @method match 17384 * @param {tinymce.ui.Control} control Control to match agains the selector. 17385 * @param {Array} selectors Optional array of selectors, mostly used internally. 17386 * @return {Boolean} true/false state if the control matches or not. 17387 */ 17388 match: function(control, selectors) { 17389 var i, l, si, sl, selector, fi, fl, filters, index, length, siblings, count, item; 17390 17391 selectors = selectors || this._selectors; 17392 for (i = 0, l = selectors.length; i < l; i++) { 17393 selector = selectors[i]; 17394 sl = selector.length; 17395 item = control; 17396 count = 0; 17397 17398 for (si = sl - 1; si >= 0; si--) { 17399 filters = selector[si]; 17400 17401 while (item) { 17402 // Find the index and length since a psuedo filter like :first needs it 17403 if (filters.psuedo) { 17404 siblings = item.parent().items(); 17405 index = length = siblings.length; 17406 while (index--) { 17407 if (siblings[index] === item) { 17408 break; 17409 } 17410 } 17411 } 17412 17413 for (fi = 0, fl = filters.length; fi < fl; fi++) { 17414 if (!filters[fi](item, index, length)) { 17415 fi = fl + 1; 17416 break; 17417 } 17418 } 17419 17420 if (fi === fl) { 17421 count++; 17422 break; 17423 } else { 17424 // If it didn't match the right most expression then 17425 // break since it's no point looking at the parents 17426 if (si === sl - 1) { 17427 break; 17428 } 17429 } 17430 17431 item = item.parent(); 17432 } 17433 } 17434 17435 // If we found all selectors then return true otherwise continue looking 17436 if (count === sl) { 17437 return true; 17438 } 17439 } 17440 17441 return false; 17442 }, 17443 17444 /** 17445 * Returns a tinymce.ui.Collection with matches of the specified selector inside the specified container. 17446 * 17447 * @method find 17448 * @param {tinymce.ui.Control} container Container to look for items in. 17449 * @return {tinymce.ui.Collection} Collection with matched elements. 17450 */ 17451 find: function(container) { 17452 var matches = [], i, l, selectors = this._selectors; 17453 17454 function collect(items, selector, index) { 17455 var i, l, fi, fl, item, filters = selector[index]; 17456 17457 for (i = 0, l = items.length; i < l; i++) { 17458 item = items[i]; 17459 17460 // Run each filter agains the item 17461 for (fi = 0, fl = filters.length; fi < fl; fi++) { 17462 if (!filters[fi](item, i, l)) { 17463 fi = fl + 1; 17464 break; 17465 } 17466 } 17467 17468 // All filters matched the item 17469 if (fi === fl) { 17470 // Matched item is on the last expression like: panel toolbar [button] 17471 if (index == selector.length - 1) { 17472 matches.push(item); 17473 } else { 17474 // Collect next expression type 17475 if (item.items) { 17476 collect(item.items(), selector, index + 1); 17477 } 17478 } 17479 } else if (filters.direct) { 17480 return; 17481 } 17482 17483 // Collect child items 17484 if (item.items) { 17485 collect(item.items(), selector, index); 17486 } 17487 } 17488 } 17489 17490 if (container.items) { 17491 for (i = 0, l = selectors.length; i < l; i++) { 17492 collect(container.items(), selectors[i], 0); 17493 } 17494 17495 // Unique the matches if needed 17496 if (l > 1) { 17497 matches = unique(matches); 17498 } 17499 } 17500 17501 // Fix for circular reference 17502 if (!Collection) { 17503 // TODO: Fix me! 17504 Collection = Selector.Collection; 17505 } 17506 17507 return new Collection(matches); 17508 } 17509 }); 17510 17511 return Selector; 17512 }); 17513 17514 // Included from: js/tinymce/classes/ui/Collection.js 17515 17516 /** 17517 * Collection.js 17518 * 17519 * Copyright, Moxiecode Systems AB 17520 * Released under LGPL License. 17521 * 17522 * License: http://www.tinymce.com/license 17523 * Contributing: http://www.tinymce.com/contributing 17524 */ 17525 17526 /** 17527 * Control collection, this class contains control instances and it enables you to 17528 * perform actions on all the contained items. This is very similar to how jQuery works. 17529 * 17530 * @example 17531 * someCollection.show().disabled(true); 17532 * 17533 * @class tinymce.ui.Collection 17534 */ 17535 define("tinymce/ui/Collection", [ 17536 "tinymce/util/Tools", 17537 "tinymce/ui/Selector", 17538 "tinymce/util/Class" 17539 ], function(Tools, Selector, Class) { 17540 "use strict"; 17541 17542 var Collection, proto, push = Array.prototype.push, slice = Array.prototype.slice; 17543 17544 proto = { 17545 /** 17546 * Current number of contained control instances. 17547 * 17548 * @field length 17549 * @type Number 17550 */ 17551 length: 0, 17552 17553 /** 17554 * Constructor for the collection. 17555 * 17556 * @constructor 17557 * @method init 17558 * @param {Array} items Optional array with items to add. 17559 */ 17560 init: function(items) { 17561 if (items) { 17562 this.add(items); 17563 } 17564 }, 17565 17566 /** 17567 * Adds new items to the control collection. 17568 * 17569 * @method add 17570 * @param {Array} items Array if items to add to collection. 17571 * @return {tinymce.ui.Collection} Current collection instance. 17572 */ 17573 add: function(items) { 17574 var self = this; 17575 17576 // Force single item into array 17577 if (!Tools.isArray(items)) { 17578 if (items instanceof Collection) { 17579 self.add(items.toArray()); 17580 } else { 17581 push.call(self, items); 17582 } 17583 } else { 17584 push.apply(self, items); 17585 } 17586 17587 return self; 17588 }, 17589 17590 /** 17591 * Sets the contents of the collection. This will remove any existing items 17592 * and replace them with the ones specified in the input array. 17593 * 17594 * @method set 17595 * @param {Array} items Array with items to set into the Collection. 17596 * @return {tinymce.ui.Collection} Collection instance. 17597 */ 17598 set: function(items) { 17599 var self = this, len = self.length, i; 17600 17601 self.length = 0; 17602 self.add(items); 17603 17604 // Remove old entries 17605 for (i = self.length; i < len; i++) { 17606 delete self[i]; 17607 } 17608 17609 return self; 17610 }, 17611 17612 /** 17613 * Filters the collection item based on the specified selector expression or selector function. 17614 * 17615 * @method filter 17616 * @param {String} selector Selector expression to filter items by. 17617 * @return {tinymce.ui.Collection} Collection containing the filtered items. 17618 */ 17619 filter: function(selector) { 17620 var self = this, i, l, matches = [], item, match; 17621 17622 // Compile string into selector expression 17623 if (typeof(selector) === "string") { 17624 selector = new Selector(selector); 17625 17626 match = function(item) { 17627 return selector.match(item); 17628 }; 17629 } else { 17630 // Use selector as matching function 17631 match = selector; 17632 } 17633 17634 for (i = 0, l = self.length; i < l; i++) { 17635 item = self[i]; 17636 17637 if (match(item)) { 17638 matches.push(item); 17639 } 17640 } 17641 17642 return new Collection(matches); 17643 }, 17644 17645 /** 17646 * Slices the items within the collection. 17647 * 17648 * @method slice 17649 * @param {Number} index Index to slice at. 17650 * @param {Number} len Optional length to slice. 17651 * @return {tinymce.ui.Collection} Current collection. 17652 */ 17653 slice: function() { 17654 return new Collection(slice.apply(this, arguments)); 17655 }, 17656 17657 /** 17658 * Makes the current collection equal to the specified index. 17659 * 17660 * @method eq 17661 * @param {Number} index Index of the item to set the collection to. 17662 * @return {tinymce.ui.Collection} Current collection. 17663 */ 17664 eq: function(index) { 17665 return index === -1 ? this.slice(index) : this.slice(index, +index + 1); 17666 }, 17667 17668 /** 17669 * Executes the specified callback on each item in collection. 17670 * 17671 * @method each 17672 * @param {function} callback Callback to execute for each item in collection. 17673 * @return {tinymce.ui.Collection} Current collection instance. 17674 */ 17675 each: function(callback) { 17676 Tools.each(this, callback); 17677 17678 return this; 17679 }, 17680 17681 /** 17682 * Returns an JavaScript array object of the contents inside the collection. 17683 * 17684 * @method toArray 17685 * @return {Array} Array with all items from collection. 17686 */ 17687 toArray: function() { 17688 return Tools.toArray(this); 17689 }, 17690 17691 /** 17692 * Finds the index of the specified control or return -1 if it isn't in the collection. 17693 * 17694 * @method indexOf 17695 * @param {Control} ctrl Control instance to look for. 17696 * @return {Number} Index of the specified control or -1. 17697 */ 17698 indexOf: function(ctrl) { 17699 var self = this, i = self.length; 17700 17701 while (i--) { 17702 if (self[i] === ctrl) { 17703 break; 17704 } 17705 } 17706 17707 return i; 17708 }, 17709 17710 /** 17711 * Returns a new collection of the contents in reverse order. 17712 * 17713 * @method reverse 17714 * @return {tinymce.ui.Collection} Collection instance with reversed items. 17715 */ 17716 reverse: function() { 17717 return new Collection(Tools.toArray(this).reverse()); 17718 }, 17719 17720 /** 17721 * Returns true/false if the class exists or not. 17722 * 17723 * @method hasClass 17724 * @param {String} cls Class to check for. 17725 * @return {Boolean} true/false state if the class exists or not. 17726 */ 17727 hasClass: function(cls) { 17728 return this[0] ? this[0].hasClass(cls) : false; 17729 }, 17730 17731 /** 17732 * Sets/gets the specific property on the items in the collection. The same as executing control.<property>(<value>); 17733 * 17734 * @method prop 17735 * @param {String} name Property name to get/set. 17736 * @param {Object} value Optional object value to set. 17737 * @return {tinymce.ui.Collection} Current collection instance or value of the first item on a get operation. 17738 */ 17739 prop: function(name, value) { 17740 var self = this, undef, item; 17741 17742 if (value !== undef) { 17743 self.each(function(item) { 17744 if (item[name]) { 17745 item[name](value); 17746 } 17747 }); 17748 17749 return self; 17750 } 17751 17752 item = self[0]; 17753 17754 if (item && item[name]) { 17755 return item[name](); 17756 } 17757 }, 17758 17759 /** 17760 * Executes the specific function name with optional arguments an all items in collection if it exists. 17761 * 17762 * @example collection.exec("myMethod", arg1, arg2, arg3); 17763 * @method exec 17764 * @param {String} name Name of the function to execute. 17765 * @param {Object} ... Multiple arguments to pass to each function. 17766 * @return {tinymce.ui.Collection} Current collection. 17767 */ 17768 exec: function(name) { 17769 var self = this, args = Tools.toArray(arguments).slice(1); 17770 17771 self.each(function(item) { 17772 if (item[name]) { 17773 item[name].apply(item, args); 17774 } 17775 }); 17776 17777 return self; 17778 }, 17779 17780 /** 17781 * Remove all items from collection and DOM. 17782 * 17783 * @method remove 17784 * @return {tinymce.ui.Collection} Current collection. 17785 */ 17786 remove: function() { 17787 var i = this.length; 17788 17789 while (i--) { 17790 this[i].remove(); 17791 } 17792 17793 return this; 17794 } 17795 17796 /** 17797 * Fires the specified event by name and arguments on the control. This will execute all 17798 * bound event handlers. 17799 * 17800 * @method fire 17801 * @param {String} name Name of the event to fire. 17802 * @param {Object} args Optional arguments to pass to the event. 17803 * @return {tinymce.ui.Collection} Current collection instance. 17804 */ 17805 // fire: function(event, args) {}, -- Generated by code below 17806 17807 /** 17808 * Binds a callback to the specified event. This event can both be 17809 * native browser events like "click" or custom ones like PostRender. 17810 * 17811 * The callback function will have two parameters the first one being the control that received the event 17812 * the second one will be the event object either the browsers native event object or a custom JS object. 17813 * 17814 * @method on 17815 * @param {String} name Name of the event to bind. For example "click". 17816 * @param {String/function} callback Callback function to execute ones the event occurs. 17817 * @return {tinymce.ui.Collection} Current collection instance. 17818 */ 17819 // on: function(name, callback) {}, -- Generated by code below 17820 17821 /** 17822 * Unbinds the specified event and optionally a specific callback. If you omit the name 17823 * parameter all event handlers will be removed. If you omit the callback all event handles 17824 * by the specified name will be removed. 17825 * 17826 * @method off 17827 * @param {String} name Optional name for the event to unbind. 17828 * @param {function} callback Optional callback function to unbind. 17829 * @return {tinymce.ui.Collection} Current collection instance. 17830 */ 17831 // off: function(name, callback) {}, -- Generated by code below 17832 17833 /** 17834 * Shows the items in the current collection. 17835 * 17836 * @method show 17837 * @return {tinymce.ui.Collection} Current collection instance. 17838 */ 17839 // show: function() {}, -- Generated by code below 17840 17841 /** 17842 * Hides the items in the current collection. 17843 * 17844 * @method hide 17845 * @return {tinymce.ui.Collection} Current collection instance. 17846 */ 17847 // hide: function() {}, -- Generated by code below 17848 17849 /** 17850 * Sets/gets the text contents of the items in the current collection. 17851 * 17852 * @method text 17853 * @return {tinymce.ui.Collection} Current collection instance or text value of the first item on a get operation. 17854 */ 17855 // text: function(value) {}, -- Generated by code below 17856 17857 /** 17858 * Sets/gets the name contents of the items in the current collection. 17859 * 17860 * @method name 17861 * @return {tinymce.ui.Collection} Current collection instance or name value of the first item on a get operation. 17862 */ 17863 // name: function(value) {}, -- Generated by code below 17864 17865 /** 17866 * Sets/gets the disabled state on the items in the current collection. 17867 * 17868 * @method disabled 17869 * @return {tinymce.ui.Collection} Current collection instance or disabled state of the first item on a get operation. 17870 */ 17871 // disabled: function(state) {}, -- Generated by code below 17872 17873 /** 17874 * Sets/gets the active state on the items in the current collection. 17875 * 17876 * @method active 17877 * @return {tinymce.ui.Collection} Current collection instance or active state of the first item on a get operation. 17878 */ 17879 // active: function(state) {}, -- Generated by code below 17880 17881 /** 17882 * Sets/gets the selected state on the items in the current collection. 17883 * 17884 * @method selected 17885 * @return {tinymce.ui.Collection} Current collection instance or selected state of the first item on a get operation. 17886 */ 17887 // selected: function(state) {}, -- Generated by code below 17888 17889 /** 17890 * Sets/gets the selected state on the items in the current collection. 17891 * 17892 * @method visible 17893 * @return {tinymce.ui.Collection} Current collection instance or visible state of the first item on a get operation. 17894 */ 17895 // visible: function(state) {}, -- Generated by code below 17896 17897 /** 17898 * Adds a class to all items in the collection. 17899 * 17900 * @method addClass 17901 * @param {String} cls Class to add to each item. 17902 * @return {tinymce.ui.Collection} Current collection instance. 17903 */ 17904 // addClass: function(cls) {}, -- Generated by code below 17905 17906 /** 17907 * Removes the specified class from all items in collection. 17908 * 17909 * @method removeClass 17910 * @param {String} cls Class to remove from each item. 17911 * @return {tinymce.ui.Collection} Current collection instance. 17912 */ 17913 // removeClass: function(cls) {}, -- Generated by code below 17914 }; 17915 17916 // Extend tinymce.ui.Collection prototype with some generated control specific methods 17917 Tools.each('fire on off show hide addClass removeClass append prepend before after reflow'.split(' '), function(name) { 17918 proto[name] = function() { 17919 var args = Tools.toArray(arguments); 17920 17921 this.each(function(ctrl) { 17922 if (name in ctrl) { 17923 ctrl[name].apply(ctrl, args); 17924 } 17925 }); 17926 17927 return this; 17928 }; 17929 }); 17930 17931 // Extend tinymce.ui.Collection prototype with some property methods 17932 Tools.each('text name disabled active selected checked visible parent value data'.split(' '), function(name) { 17933 proto[name] = function(value) { 17934 return this.prop(name, value); 17935 }; 17936 }); 17937 17938 // Create class based on the new prototype 17939 Collection = Class.extend(proto); 17940 17941 // Stick Collection into Selector to prevent circual references 17942 Selector.Collection = Collection; 17943 17944 return Collection; 17945 }); 17946 17947 // Included from: js/tinymce/classes/ui/DomUtils.js 17948 17949 /** 17950 * DOMUtils.js 17951 * 17952 * Copyright, Moxiecode Systems AB 17953 * Released under LGPL License. 17954 * 17955 * License: http://www.tinymce.com/license 17956 * Contributing: http://www.tinymce.com/contributing 17957 */ 17958 17959 define("tinymce/ui/DomUtils", [ 17960 "tinymce/util/Tools", 17961 "tinymce/dom/DOMUtils" 17962 ], function(Tools, DOMUtils) { 17963 "use strict"; 17964 17965 return { 17966 id: function() { 17967 return DOMUtils.DOM.uniqueId(); 17968 }, 17969 17970 createFragment: function(html) { 17971 return DOMUtils.DOM.createFragment(html); 17972 }, 17973 17974 getWindowSize: function() { 17975 return DOMUtils.DOM.getViewPort(); 17976 }, 17977 17978 getSize: function(elm) { 17979 var width, height; 17980 17981 if (elm.getBoundingClientRect) { 17982 var rect = elm.getBoundingClientRect(); 17983 17984 width = Math.max(rect.width || (rect.right - rect.left), elm.offsetWidth); 17985 height = Math.max(rect.height || (rect.bottom - rect.bottom), elm.offsetHeight); 17986 } else { 17987 width = elm.offsetWidth; 17988 height = elm.offsetHeight; 17989 } 17990 17991 return {width: width, height: height}; 17992 }, 17993 17994 getPos: function(elm, root) { 17995 return DOMUtils.DOM.getPos(elm, root); 17996 }, 17997 17998 getViewPort: function(win) { 17999 return DOMUtils.DOM.getViewPort(win); 18000 }, 18001 18002 get: function(id) { 18003 return document.getElementById(id); 18004 }, 18005 18006 addClass : function(elm, cls) { 18007 return DOMUtils.DOM.addClass(elm, cls); 18008 }, 18009 18010 removeClass : function(elm, cls) { 18011 return DOMUtils.DOM.removeClass(elm, cls); 18012 }, 18013 18014 hasClass : function(elm, cls) { 18015 return DOMUtils.DOM.hasClass(elm, cls); 18016 }, 18017 18018 toggleClass: function(elm, cls, state) { 18019 return DOMUtils.DOM.toggleClass(elm, cls, state); 18020 }, 18021 18022 css: function(elm, name, value) { 18023 return DOMUtils.DOM.setStyle(elm, name, value); 18024 }, 18025 18026 on: function(target, name, callback, scope) { 18027 return DOMUtils.DOM.bind(target, name, callback, scope); 18028 }, 18029 18030 off: function(target, name, callback) { 18031 return DOMUtils.DOM.unbind(target, name, callback); 18032 }, 18033 18034 fire: function(target, name, args) { 18035 return DOMUtils.DOM.fire(target, name, args); 18036 }, 18037 18038 innerHtml: function(elm, html) { 18039 // Workaround for <div> in <p> bug on IE 8 #6178 18040 DOMUtils.DOM.setHTML(elm, html); 18041 } 18042 }; 18043 }); 18044 18045 // Included from: js/tinymce/classes/ui/Control.js 18046 18047 /** 18048 * Control.js 18049 * 18050 * Copyright, Moxiecode Systems AB 18051 * Released under LGPL License. 18052 * 18053 * License: http://www.tinymce.com/license 18054 * Contributing: http://www.tinymce.com/contributing 18055 */ 18056 18057 /*eslint consistent-this:0 */ 18058 18059 /** 18060 * This is the base class for all controls and containers. All UI control instances inherit 18061 * from this one as it has the base logic needed by all of them. 18062 * 18063 * @class tinymce.ui.Control 18064 */ 18065 define("tinymce/ui/Control", [ 18066 "tinymce/util/Class", 18067 "tinymce/util/Tools", 18068 "tinymce/util/EventDispatcher", 18069 "tinymce/ui/Collection", 18070 "tinymce/ui/DomUtils" 18071 ], function(Class, Tools, EventDispatcher, Collection, DomUtils) { 18072 "use strict"; 18073 18074 var elementIdCache = {}; 18075 var hasMouseWheelEventSupport = "onmousewheel" in document; 18076 var hasWheelEventSupport = false; 18077 var classPrefix = "mce-"; 18078 18079 function getEventDispatcher(obj) { 18080 if (!obj._eventDispatcher) { 18081 obj._eventDispatcher = new EventDispatcher({ 18082 scope: obj, 18083 toggleEvent: function(name, state) { 18084 if (state && EventDispatcher.isNative(name)) { 18085 if (!obj._nativeEvents) { 18086 obj._nativeEvents = {}; 18087 } 18088 18089 obj._nativeEvents[name] = true; 18090 18091 if (obj._rendered) { 18092 obj.bindPendingEvents(); 18093 } 18094 } 18095 } 18096 }); 18097 } 18098 18099 return obj._eventDispatcher; 18100 } 18101 18102 var Control = Class.extend({ 18103 Statics: { 18104 elementIdCache: elementIdCache, 18105 classPrefix: classPrefix 18106 }, 18107 18108 isRtl: function() { 18109 return Control.rtl; 18110 }, 18111 18112 /** 18113 * Class/id prefix to use for all controls. 18114 * 18115 * @final 18116 * @field {String} classPrefix 18117 */ 18118 classPrefix: classPrefix, 18119 18120 /** 18121 * Constructs a new control instance with the specified settings. 18122 * 18123 * @constructor 18124 * @param {Object} settings Name/value object with settings. 18125 * @setting {String} style Style CSS properties to add. 18126 * @setting {String} border Border box values example: 1 1 1 1 18127 * @setting {String} padding Padding box values example: 1 1 1 1 18128 * @setting {String} margin Margin box values example: 1 1 1 1 18129 * @setting {Number} minWidth Minimal width for the control. 18130 * @setting {Number} minHeight Minimal height for the control. 18131 * @setting {String} classes Space separated list of classes to add. 18132 * @setting {String} role WAI-ARIA role to use for control. 18133 * @setting {Boolean} hidden Is the control hidden by default. 18134 * @setting {Boolean} disabled Is the control disabled by default. 18135 * @setting {String} name Name of the control instance. 18136 */ 18137 init: function(settings) { 18138 var self = this, classes, i; 18139 18140 self.settings = settings = Tools.extend({}, self.Defaults, settings); 18141 18142 // Initial states 18143 self._id = settings.id || DomUtils.id(); 18144 self._text = self._name = ''; 18145 self._width = self._height = 0; 18146 self._aria = {role: settings.role}; 18147 18148 // Setup classes 18149 classes = settings.classes; 18150 if (classes) { 18151 classes = classes.split(' '); 18152 classes.map = {}; 18153 i = classes.length; 18154 while (i--) { 18155 classes.map[classes[i]] = true; 18156 } 18157 } 18158 18159 self._classes = classes || []; 18160 self.visible(true); 18161 18162 // Set some properties 18163 Tools.each('title text width height name classes visible disabled active value'.split(' '), function(name) { 18164 var value = settings[name], undef; 18165 18166 if (value !== undef) { 18167 self[name](value); 18168 } else if (self['_' + name] === undef) { 18169 self['_' + name] = false; 18170 } 18171 }); 18172 18173 self.on('click', function() { 18174 if (self.disabled()) { 18175 return false; 18176 } 18177 }); 18178 18179 // TODO: Is this needed duplicate code see above? 18180 if (settings.classes) { 18181 Tools.each(settings.classes.split(' '), function(cls) { 18182 self.addClass(cls); 18183 }); 18184 } 18185 18186 /** 18187 * Name/value object with settings for the current control. 18188 * 18189 * @field {Object} settings 18190 */ 18191 self.settings = settings; 18192 18193 self._borderBox = self.parseBox(settings.border); 18194 self._paddingBox = self.parseBox(settings.padding); 18195 self._marginBox = self.parseBox(settings.margin); 18196 18197 if (settings.hidden) { 18198 self.hide(); 18199 } 18200 }, 18201 18202 // Will generate getter/setter methods for these properties 18203 Properties: 'parent,title,text,width,height,disabled,active,name,value', 18204 18205 // Will generate empty dummy functions for these 18206 Methods: 'renderHtml', 18207 18208 /** 18209 * Returns the root element to render controls into. 18210 * 18211 * @method getContainerElm 18212 * @return {Element} HTML DOM element to render into. 18213 */ 18214 getContainerElm: function() { 18215 return document.body; 18216 }, 18217 18218 /** 18219 * Returns a control instance for the current DOM element. 18220 * 18221 * @method getParentCtrl 18222 * @param {Element} elm HTML dom element to get parent control from. 18223 * @return {tinymce.ui.Control} Control instance or undefined. 18224 */ 18225 getParentCtrl: function(elm) { 18226 var ctrl, lookup = this.getRoot().controlIdLookup; 18227 18228 while (elm && lookup) { 18229 ctrl = lookup[elm.id]; 18230 if (ctrl) { 18231 break; 18232 } 18233 18234 elm = elm.parentNode; 18235 } 18236 18237 return ctrl; 18238 }, 18239 18240 /** 18241 * Parses the specified box value. A box value contains 1-4 properties in clockwise order. 18242 * 18243 * @method parseBox 18244 * @param {String/Number} value Box value "0 1 2 3" or "0" etc. 18245 * @return {Object} Object with top/right/bottom/left properties. 18246 * @private 18247 */ 18248 parseBox: function(value) { 18249 var len, radix = 10; 18250 18251 if (!value) { 18252 return; 18253 } 18254 18255 if (typeof(value) === "number") { 18256 value = value || 0; 18257 18258 return { 18259 top: value, 18260 left: value, 18261 bottom: value, 18262 right: value 18263 }; 18264 } 18265 18266 value = value.split(' '); 18267 len = value.length; 18268 18269 if (len === 1) { 18270 value[1] = value[2] = value[3] = value[0]; 18271 } else if (len === 2) { 18272 value[2] = value[0]; 18273 value[3] = value[1]; 18274 } else if (len === 3) { 18275 value[3] = value[1]; 18276 } 18277 18278 return { 18279 top: parseInt(value[0], radix) || 0, 18280 right: parseInt(value[1], radix) || 0, 18281 bottom: parseInt(value[2], radix) || 0, 18282 left: parseInt(value[3], radix) || 0 18283 }; 18284 }, 18285 18286 borderBox: function() { 18287 return this._borderBox; 18288 }, 18289 18290 paddingBox: function() { 18291 return this._paddingBox; 18292 }, 18293 18294 marginBox: function() { 18295 return this._marginBox; 18296 }, 18297 18298 measureBox: function(elm, prefix) { 18299 function getStyle(name) { 18300 var defaultView = document.defaultView; 18301 18302 if (defaultView) { 18303 // Remove camelcase 18304 name = name.replace(/[A-Z]/g, function(a) { 18305 return '-' + a; 18306 }); 18307 18308 return defaultView.getComputedStyle(elm, null).getPropertyValue(name); 18309 } 18310 18311 return elm.currentStyle[name]; 18312 } 18313 18314 function getSide(name) { 18315 var val = parseFloat(getStyle(name), 10); 18316 18317 return isNaN(val) ? 0 : val; 18318 } 18319 18320 return { 18321 top: getSide(prefix + "TopWidth"), 18322 right: getSide(prefix + "RightWidth"), 18323 bottom: getSide(prefix + "BottomWidth"), 18324 left: getSide(prefix + "LeftWidth") 18325 }; 18326 }, 18327 18328 /** 18329 * Initializes the current controls layout rect. 18330 * This will be executed by the layout managers to determine the 18331 * default minWidth/minHeight etc. 18332 * 18333 * @method initLayoutRect 18334 * @return {Object} Layout rect instance. 18335 */ 18336 initLayoutRect: function() { 18337 var self = this, settings = self.settings, borderBox, layoutRect; 18338 var elm = self.getEl(), width, height, minWidth, minHeight, autoResize; 18339 var startMinWidth, startMinHeight, initialSize; 18340 18341 // Measure the current element 18342 borderBox = self._borderBox = self._borderBox || self.measureBox(elm, 'border'); 18343 self._paddingBox = self._paddingBox || self.measureBox(elm, 'padding'); 18344 self._marginBox = self._marginBox || self.measureBox(elm, 'margin'); 18345 initialSize = DomUtils.getSize(elm); 18346 18347 // Setup minWidth/minHeight and width/height 18348 startMinWidth = settings.minWidth; 18349 startMinHeight = settings.minHeight; 18350 minWidth = startMinWidth || initialSize.width; 18351 minHeight = startMinHeight || initialSize.height; 18352 width = settings.width; 18353 height = settings.height; 18354 autoResize = settings.autoResize; 18355 autoResize = typeof(autoResize) != "undefined" ? autoResize : !width && !height; 18356 18357 width = width || minWidth; 18358 height = height || minHeight; 18359 18360 var deltaW = borderBox.left + borderBox.right; 18361 var deltaH = borderBox.top + borderBox.bottom; 18362 18363 var maxW = settings.maxWidth || 0xFFFF; 18364 var maxH = settings.maxHeight || 0xFFFF; 18365 18366 // Setup initial layout rect 18367 self._layoutRect = layoutRect = { 18368 x: settings.x || 0, 18369 y: settings.y || 0, 18370 w: width, 18371 h: height, 18372 deltaW: deltaW, 18373 deltaH: deltaH, 18374 contentW: width - deltaW, 18375 contentH: height - deltaH, 18376 innerW: width - deltaW, 18377 innerH: height - deltaH, 18378 startMinWidth: startMinWidth || 0, 18379 startMinHeight: startMinHeight || 0, 18380 minW: Math.min(minWidth, maxW), 18381 minH: Math.min(minHeight, maxH), 18382 maxW: maxW, 18383 maxH: maxH, 18384 autoResize: autoResize, 18385 scrollW: 0 18386 }; 18387 18388 self._lastLayoutRect = {}; 18389 18390 return layoutRect; 18391 }, 18392 18393 /** 18394 * Getter/setter for the current layout rect. 18395 * 18396 * @method layoutRect 18397 * @param {Object} [newRect] Optional new layout rect. 18398 * @return {tinymce.ui.Control/Object} Current control or rect object. 18399 */ 18400 layoutRect: function(newRect) { 18401 var self = this, curRect = self._layoutRect, lastLayoutRect, size, deltaWidth, deltaHeight, undef, repaintControls; 18402 18403 // Initialize default layout rect 18404 if (!curRect) { 18405 curRect = self.initLayoutRect(); 18406 } 18407 18408 // Set new rect values 18409 if (newRect) { 18410 // Calc deltas between inner and outer sizes 18411 deltaWidth = curRect.deltaW; 18412 deltaHeight = curRect.deltaH; 18413 18414 // Set x position 18415 if (newRect.x !== undef) { 18416 curRect.x = newRect.x; 18417 } 18418 18419 // Set y position 18420 if (newRect.y !== undef) { 18421 curRect.y = newRect.y; 18422 } 18423 18424 // Set minW 18425 if (newRect.minW !== undef) { 18426 curRect.minW = newRect.minW; 18427 } 18428 18429 // Set minH 18430 if (newRect.minH !== undef) { 18431 curRect.minH = newRect.minH; 18432 } 18433 18434 // Set new width and calculate inner width 18435 size = newRect.w; 18436 if (size !== undef) { 18437 size = size < curRect.minW ? curRect.minW : size; 18438 size = size > curRect.maxW ? curRect.maxW : size; 18439 curRect.w = size; 18440 curRect.innerW = size - deltaWidth; 18441 } 18442 18443 // Set new height and calculate inner height 18444 size = newRect.h; 18445 if (size !== undef) { 18446 size = size < curRect.minH ? curRect.minH : size; 18447 size = size > curRect.maxH ? curRect.maxH : size; 18448 curRect.h = size; 18449 curRect.innerH = size - deltaHeight; 18450 } 18451 18452 // Set new inner width and calculate width 18453 size = newRect.innerW; 18454 if (size !== undef) { 18455 size = size < curRect.minW - deltaWidth ? curRect.minW - deltaWidth : size; 18456 size = size > curRect.maxW - deltaWidth ? curRect.maxW - deltaWidth : size; 18457 curRect.innerW = size; 18458 curRect.w = size + deltaWidth; 18459 } 18460 18461 // Set new height and calculate inner height 18462 size = newRect.innerH; 18463 if (size !== undef) { 18464 size = size < curRect.minH - deltaHeight ? curRect.minH - deltaHeight : size; 18465 size = size > curRect.maxH - deltaHeight ? curRect.maxH - deltaHeight : size; 18466 curRect.innerH = size; 18467 curRect.h = size + deltaHeight; 18468 } 18469 18470 // Set new contentW 18471 if (newRect.contentW !== undef) { 18472 curRect.contentW = newRect.contentW; 18473 } 18474 18475 // Set new contentH 18476 if (newRect.contentH !== undef) { 18477 curRect.contentH = newRect.contentH; 18478 } 18479 18480 // Compare last layout rect with the current one to see if we need to repaint or not 18481 lastLayoutRect = self._lastLayoutRect; 18482 if (lastLayoutRect.x !== curRect.x || lastLayoutRect.y !== curRect.y || 18483 lastLayoutRect.w !== curRect.w || lastLayoutRect.h !== curRect.h) { 18484 repaintControls = Control.repaintControls; 18485 18486 if (repaintControls) { 18487 if (repaintControls.map && !repaintControls.map[self._id]) { 18488 repaintControls.push(self); 18489 repaintControls.map[self._id] = true; 18490 } 18491 } 18492 18493 lastLayoutRect.x = curRect.x; 18494 lastLayoutRect.y = curRect.y; 18495 lastLayoutRect.w = curRect.w; 18496 lastLayoutRect.h = curRect.h; 18497 } 18498 18499 return self; 18500 } 18501 18502 return curRect; 18503 }, 18504 18505 /** 18506 * Repaints the control after a layout operation. 18507 * 18508 * @method repaint 18509 */ 18510 repaint: function() { 18511 var self = this, style, bodyStyle, rect, borderBox, borderW = 0, borderH = 0, lastRepaintRect, round; 18512 18513 // Use Math.round on all values on IE < 9 18514 round = !document.createRange ? Math.round : function(value) { 18515 return value; 18516 }; 18517 18518 style = self.getEl().style; 18519 rect = self._layoutRect; 18520 lastRepaintRect = self._lastRepaintRect || {}; 18521 18522 borderBox = self._borderBox; 18523 borderW = borderBox.left + borderBox.right; 18524 borderH = borderBox.top + borderBox.bottom; 18525 18526 if (rect.x !== lastRepaintRect.x) { 18527 style.left = round(rect.x) + 'px'; 18528 lastRepaintRect.x = rect.x; 18529 } 18530 18531 if (rect.y !== lastRepaintRect.y) { 18532 style.top = round(rect.y) + 'px'; 18533 lastRepaintRect.y = rect.y; 18534 } 18535 18536 if (rect.w !== lastRepaintRect.w) { 18537 style.width = round(rect.w - borderW) + 'px'; 18538 lastRepaintRect.w = rect.w; 18539 } 18540 18541 if (rect.h !== lastRepaintRect.h) { 18542 style.height = round(rect.h - borderH) + 'px'; 18543 lastRepaintRect.h = rect.h; 18544 } 18545 18546 // Update body if needed 18547 if (self._hasBody && rect.innerW !== lastRepaintRect.innerW) { 18548 bodyStyle = self.getEl('body').style; 18549 bodyStyle.width = round(rect.innerW) + 'px'; 18550 lastRepaintRect.innerW = rect.innerW; 18551 } 18552 18553 if (self._hasBody && rect.innerH !== lastRepaintRect.innerH) { 18554 bodyStyle = bodyStyle || self.getEl('body').style; 18555 bodyStyle.height = round(rect.innerH) + 'px'; 18556 lastRepaintRect.innerH = rect.innerH; 18557 } 18558 18559 self._lastRepaintRect = lastRepaintRect; 18560 self.fire('repaint', {}, false); 18561 }, 18562 18563 /** 18564 * Binds a callback to the specified event. This event can both be 18565 * native browser events like "click" or custom ones like PostRender. 18566 * 18567 * The callback function will be passed a DOM event like object that enables yout do stop propagation. 18568 * 18569 * @method on 18570 * @param {String} name Name of the event to bind. For example "click". 18571 * @param {String/function} callback Callback function to execute ones the event occurs. 18572 * @return {tinymce.ui.Control} Current control object. 18573 */ 18574 on: function(name, callback) { 18575 var self = this; 18576 18577 function resolveCallbackName(name) { 18578 var callback, scope; 18579 18580 if (typeof(name) != 'string') { 18581 return name; 18582 } 18583 18584 return function(e) { 18585 if (!callback) { 18586 self.parentsAndSelf().each(function(ctrl) { 18587 var callbacks = ctrl.settings.callbacks; 18588 18589 if (callbacks && (callback = callbacks[name])) { 18590 scope = ctrl; 18591 return false; 18592 } 18593 }); 18594 } 18595 18596 return callback.call(scope, e); 18597 }; 18598 } 18599 18600 getEventDispatcher(self).on(name, resolveCallbackName(callback)); 18601 18602 return self; 18603 }, 18604 18605 /** 18606 * Unbinds the specified event and optionally a specific callback. If you omit the name 18607 * parameter all event handlers will be removed. If you omit the callback all event handles 18608 * by the specified name will be removed. 18609 * 18610 * @method off 18611 * @param {String} [name] Name for the event to unbind. 18612 * @param {function} [callback] Callback function to unbind. 18613 * @return {mxex.ui.Control} Current control object. 18614 */ 18615 off: function(name, callback) { 18616 getEventDispatcher(this).off(name, callback); 18617 return this; 18618 }, 18619 18620 /** 18621 * Fires the specified event by name and arguments on the control. This will execute all 18622 * bound event handlers. 18623 * 18624 * @method fire 18625 * @param {String} name Name of the event to fire. 18626 * @param {Object} [args] Arguments to pass to the event. 18627 * @param {Boolean} [bubble] Value to control bubbeling. Defaults to true. 18628 * @return {Object} Current arguments object. 18629 */ 18630 fire: function(name, args, bubble) { 18631 var self = this; 18632 18633 args = args || {}; 18634 18635 if (!args.control) { 18636 args.control = self; 18637 } 18638 18639 args = getEventDispatcher(self).fire(name, args); 18640 18641 // Bubble event up to parents 18642 if (bubble !== false && self.parent) { 18643 var parent = self.parent(); 18644 while (parent && !args.isPropagationStopped()) { 18645 parent.fire(name, args, false); 18646 parent = parent.parent(); 18647 } 18648 } 18649 18650 return args; 18651 }, 18652 18653 /** 18654 * Returns true/false if the specified event has any listeners. 18655 * 18656 * @method hasEventListeners 18657 * @param {String} name Name of the event to check for. 18658 * @return {Boolean} True/false state if the event has listeners. 18659 */ 18660 hasEventListeners: function(name) { 18661 return getEventDispatcher(this).has(name); 18662 }, 18663 18664 /** 18665 * Returns a control collection with all parent controls. 18666 * 18667 * @method parents 18668 * @param {String} selector Optional selector expression to find parents. 18669 * @return {tinymce.ui.Collection} Collection with all parent controls. 18670 */ 18671 parents: function(selector) { 18672 var self = this, ctrl, parents = new Collection(); 18673 18674 // Add each parent to collection 18675 for (ctrl = self.parent(); ctrl; ctrl = ctrl.parent()) { 18676 parents.add(ctrl); 18677 } 18678 18679 // Filter away everything that doesn't match the selector 18680 if (selector) { 18681 parents = parents.filter(selector); 18682 } 18683 18684 return parents; 18685 }, 18686 18687 /** 18688 * Returns the current control and it's parents. 18689 * 18690 * @method parentsAndSelf 18691 * @param {String} selector Optional selector expression to find parents. 18692 * @return {tinymce.ui.Collection} Collection with all parent controls. 18693 */ 18694 parentsAndSelf: function(selector) { 18695 return new Collection(this).add(this.parents(selector)); 18696 }, 18697 18698 /** 18699 * Returns the control next to the current control. 18700 * 18701 * @method next 18702 * @return {tinymce.ui.Control} Next control instance. 18703 */ 18704 next: function() { 18705 var parentControls = this.parent().items(); 18706 18707 return parentControls[parentControls.indexOf(this) + 1]; 18708 }, 18709 18710 /** 18711 * Returns the control previous to the current control. 18712 * 18713 * @method prev 18714 * @return {tinymce.ui.Control} Previous control instance. 18715 */ 18716 prev: function() { 18717 var parentControls = this.parent().items(); 18718 18719 return parentControls[parentControls.indexOf(this) - 1]; 18720 }, 18721 18722 /** 18723 * Find the common ancestor for two control instances. 18724 * 18725 * @method findCommonAncestor 18726 * @param {tinymce.ui.Control} ctrl1 First control. 18727 * @param {tinymce.ui.Control} ctrl2 Second control. 18728 * @return {tinymce.ui.Control} Ancestor control instance. 18729 */ 18730 findCommonAncestor: function(ctrl1, ctrl2) { 18731 var parentCtrl; 18732 18733 while (ctrl1) { 18734 parentCtrl = ctrl2; 18735 18736 while (parentCtrl && ctrl1 != parentCtrl) { 18737 parentCtrl = parentCtrl.parent(); 18738 } 18739 18740 if (ctrl1 == parentCtrl) { 18741 break; 18742 } 18743 18744 ctrl1 = ctrl1.parent(); 18745 } 18746 18747 return ctrl1; 18748 }, 18749 18750 /** 18751 * Returns true/false if the specific control has the specific class. 18752 * 18753 * @method hasClass 18754 * @param {String} cls Class to check for. 18755 * @param {String} [group] Sub element group name. 18756 * @return {Boolean} True/false if the control has the specified class. 18757 */ 18758 hasClass: function(cls, group) { 18759 var classes = this._classes[group || 'control']; 18760 18761 cls = this.classPrefix + cls; 18762 18763 return classes && !!classes.map[cls]; 18764 }, 18765 18766 /** 18767 * Adds the specified class to the control 18768 * 18769 * @method addClass 18770 * @param {String} cls Class to check for. 18771 * @param {String} [group] Sub element group name. 18772 * @return {tinymce.ui.Control} Current control object. 18773 */ 18774 addClass: function(cls, group) { 18775 var self = this, classes, elm; 18776 18777 cls = this.classPrefix + cls; 18778 classes = self._classes[group || 'control']; 18779 18780 if (!classes) { 18781 classes = []; 18782 classes.map = {}; 18783 self._classes[group || 'control'] = classes; 18784 } 18785 18786 if (!classes.map[cls]) { 18787 classes.map[cls] = cls; 18788 classes.push(cls); 18789 18790 if (self._rendered) { 18791 elm = self.getEl(group); 18792 18793 if (elm) { 18794 elm.className = classes.join(' '); 18795 } 18796 } 18797 } 18798 18799 return self; 18800 }, 18801 18802 /** 18803 * Removes the specified class from the control. 18804 * 18805 * @method removeClass 18806 * @param {String} cls Class to remove. 18807 * @param {String} [group] Sub element group name. 18808 * @return {tinymce.ui.Control} Current control object. 18809 */ 18810 removeClass: function(cls, group) { 18811 var self = this, classes, i, elm; 18812 18813 cls = this.classPrefix + cls; 18814 classes = self._classes[group || 'control']; 18815 if (classes && classes.map[cls]) { 18816 delete classes.map[cls]; 18817 18818 i = classes.length; 18819 while (i--) { 18820 if (classes[i] === cls) { 18821 classes.splice(i, 1); 18822 } 18823 } 18824 } 18825 18826 if (self._rendered) { 18827 elm = self.getEl(group); 18828 18829 if (elm) { 18830 elm.className = classes.join(' '); 18831 } 18832 } 18833 18834 return self; 18835 }, 18836 18837 /** 18838 * Toggles the specified class on the control. 18839 * 18840 * @method toggleClass 18841 * @param {String} cls Class to remove. 18842 * @param {Boolean} state True/false state to add/remove class. 18843 * @param {String} [group] Sub element group name. 18844 * @return {tinymce.ui.Control} Current control object. 18845 */ 18846 toggleClass: function(cls, state, group) { 18847 var self = this; 18848 18849 if (state) { 18850 self.addClass(cls, group); 18851 } else { 18852 self.removeClass(cls, group); 18853 } 18854 18855 return self; 18856 }, 18857 18858 /** 18859 * Returns the class string for the specified group name. 18860 * 18861 * @method classes 18862 * @param {String} [group] Group to get clases by. 18863 * @return {String} Classes for the specified group. 18864 */ 18865 classes: function(group) { 18866 var classes = this._classes[group || 'control']; 18867 18868 return classes ? classes.join(' ') : ''; 18869 }, 18870 18871 /** 18872 * Sets the inner HTML of the control element. 18873 * 18874 * @method innerHtml 18875 * @param {String} html Html string to set as inner html. 18876 * @return {tinymce.ui.Control} Current control object. 18877 */ 18878 innerHtml: function(html) { 18879 DomUtils.innerHtml(this.getEl(), html); 18880 return this; 18881 }, 18882 18883 /** 18884 * Returns the control DOM element or sub element. 18885 * 18886 * @method getEl 18887 * @param {String} [suffix] Suffix to get element by. 18888 * @param {Boolean} [dropCache] True if the cache for the element should be dropped. 18889 * @return {Element} HTML DOM element for the current control or it's children. 18890 */ 18891 getEl: function(suffix, dropCache) { 18892 var elm, id = suffix ? this._id + '-' + suffix : this._id; 18893 18894 elm = elementIdCache[id] = (dropCache === true ? null : elementIdCache[id]) || DomUtils.get(id); 18895 18896 return elm; 18897 }, 18898 18899 /** 18900 * Sets/gets the visible for the control. 18901 * 18902 * @method visible 18903 * @param {Boolean} state Value to set to control. 18904 * @return {Boolean/tinymce.ui.Control} Current control on a set operation or current state on a get. 18905 */ 18906 visible: function(state) { 18907 var self = this, parentCtrl; 18908 18909 if (typeof(state) !== "undefined") { 18910 if (self._visible !== state) { 18911 if (self._rendered) { 18912 self.getEl().style.display = state ? '' : 'none'; 18913 } 18914 18915 self._visible = state; 18916 18917 // Parent container needs to reflow 18918 parentCtrl = self.parent(); 18919 if (parentCtrl) { 18920 parentCtrl._lastRect = null; 18921 } 18922 18923 self.fire(state ? 'show' : 'hide'); 18924 } 18925 18926 return self; 18927 } 18928 18929 return self._visible; 18930 }, 18931 18932 /** 18933 * Sets the visible state to true. 18934 * 18935 * @method show 18936 * @return {tinymce.ui.Control} Current control instance. 18937 */ 18938 show: function() { 18939 return this.visible(true); 18940 }, 18941 18942 /** 18943 * Sets the visible state to false. 18944 * 18945 * @method hide 18946 * @return {tinymce.ui.Control} Current control instance. 18947 */ 18948 hide: function() { 18949 return this.visible(false); 18950 }, 18951 18952 /** 18953 * Focuses the current control. 18954 * 18955 * @method focus 18956 * @return {tinymce.ui.Control} Current control instance. 18957 */ 18958 focus: function() { 18959 try { 18960 this.getEl().focus(); 18961 } catch (ex) { 18962 // Ignore IE error 18963 } 18964 18965 return this; 18966 }, 18967 18968 /** 18969 * Blurs the current control. 18970 * 18971 * @method blur 18972 * @return {tinymce.ui.Control} Current control instance. 18973 */ 18974 blur: function() { 18975 this.getEl().blur(); 18976 18977 return this; 18978 }, 18979 18980 /** 18981 * Sets the specified aria property. 18982 * 18983 * @method aria 18984 * @param {String} name Name of the aria property to set. 18985 * @param {String} value Value of the aria property. 18986 * @return {tinymce.ui.Control} Current control instance. 18987 */ 18988 aria: function(name, value) { 18989 var self = this, elm = self.getEl(self.ariaTarget); 18990 18991 if (typeof(value) === "undefined") { 18992 return self._aria[name]; 18993 } else { 18994 self._aria[name] = value; 18995 } 18996 18997 if (self._rendered) { 18998 elm.setAttribute(name == 'role' ? name : 'aria-' + name, value); 18999 } 19000 19001 return self; 19002 }, 19003 19004 /** 19005 * Encodes the specified string with HTML entities. It will also 19006 * translate the string to different languages. 19007 * 19008 * @method encode 19009 * @param {String/Object/Array} text Text to entity encode. 19010 * @param {Boolean} [translate=true] False if the contents shouldn't be translated. 19011 * @return {String} Encoded and possible traslated string. 19012 */ 19013 encode: function(text, translate) { 19014 if (translate !== false) { 19015 text = this.translate(text); 19016 } 19017 19018 return (text || '').replace(/[&<>"]/g, function(match) { 19019 return '' + match.charCodeAt(0) + ';'; 19020 }); 19021 }, 19022 19023 /** 19024 * Returns the translated string. 19025 * 19026 * @method translate 19027 * @param {String} text Text to translate. 19028 * @return {String} Translated string or the same as the input. 19029 */ 19030 translate: function(text) { 19031 return Control.translate ? Control.translate(text) : text; 19032 }, 19033 19034 /** 19035 * Adds items before the current control. 19036 * 19037 * @method before 19038 * @param {Array/tinymce.ui.Collection} items Array of items to prepend before this control. 19039 * @return {tinymce.ui.Control} Current control instance. 19040 */ 19041 before: function(items) { 19042 var self = this, parent = self.parent(); 19043 19044 if (parent) { 19045 parent.insert(items, parent.items().indexOf(self), true); 19046 } 19047 19048 return self; 19049 }, 19050 19051 /** 19052 * Adds items after the current control. 19053 * 19054 * @method after 19055 * @param {Array/tinymce.ui.Collection} items Array of items to append after this control. 19056 * @return {tinymce.ui.Control} Current control instance. 19057 */ 19058 after: function(items) { 19059 var self = this, parent = self.parent(); 19060 19061 if (parent) { 19062 parent.insert(items, parent.items().indexOf(self)); 19063 } 19064 19065 return self; 19066 }, 19067 19068 /** 19069 * Removes the current control from DOM and from UI collections. 19070 * 19071 * @method remove 19072 * @return {tinymce.ui.Control} Current control instance. 19073 */ 19074 remove: function() { 19075 var self = this, elm = self.getEl(), parent = self.parent(), newItems, i; 19076 19077 if (self.items) { 19078 var controls = self.items().toArray(); 19079 i = controls.length; 19080 while (i--) { 19081 controls[i].remove(); 19082 } 19083 } 19084 19085 if (parent && parent.items) { 19086 newItems = []; 19087 19088 parent.items().each(function(item) { 19089 if (item !== self) { 19090 newItems.push(item); 19091 } 19092 }); 19093 19094 parent.items().set(newItems); 19095 parent._lastRect = null; 19096 } 19097 19098 if (self._eventsRoot && self._eventsRoot == self) { 19099 DomUtils.off(elm); 19100 } 19101 19102 var lookup = self.getRoot().controlIdLookup; 19103 if (lookup) { 19104 delete lookup[self._id]; 19105 } 19106 19107 delete elementIdCache[self._id]; 19108 19109 if (elm && elm.parentNode) { 19110 var nodes = elm.getElementsByTagName('*'); 19111 19112 i = nodes.length; 19113 while (i--) { 19114 delete elementIdCache[nodes[i].id]; 19115 } 19116 19117 elm.parentNode.removeChild(elm); 19118 } 19119 19120 self._rendered = false; 19121 19122 return self; 19123 }, 19124 19125 /** 19126 * Renders the control before the specified element. 19127 * 19128 * @method renderBefore 19129 * @param {Element} elm Element to render before. 19130 * @return {tinymce.ui.Control} Current control instance. 19131 */ 19132 renderBefore: function(elm) { 19133 var self = this; 19134 19135 elm.parentNode.insertBefore(DomUtils.createFragment(self.renderHtml()), elm); 19136 self.postRender(); 19137 19138 return self; 19139 }, 19140 19141 /** 19142 * Renders the control to the specified element. 19143 * 19144 * @method renderBefore 19145 * @param {Element} elm Element to render to. 19146 * @return {tinymce.ui.Control} Current control instance. 19147 */ 19148 renderTo: function(elm) { 19149 var self = this; 19150 19151 elm = elm || self.getContainerElm(); 19152 elm.appendChild(DomUtils.createFragment(self.renderHtml())); 19153 self.postRender(); 19154 19155 return self; 19156 }, 19157 19158 /** 19159 * Post render method. Called after the control has been rendered to the target. 19160 * 19161 * @method postRender 19162 * @return {tinymce.ui.Control} Current control instance. 19163 */ 19164 postRender: function() { 19165 var self = this, settings = self.settings, elm, box, parent, name, parentEventsRoot; 19166 19167 // Bind on<event> settings 19168 for (name in settings) { 19169 if (name.indexOf("on") === 0) { 19170 self.on(name.substr(2), settings[name]); 19171 } 19172 } 19173 19174 if (self._eventsRoot) { 19175 for (parent = self.parent(); !parentEventsRoot && parent; parent = parent.parent()) { 19176 parentEventsRoot = parent._eventsRoot; 19177 } 19178 19179 if (parentEventsRoot) { 19180 for (name in parentEventsRoot._nativeEvents) { 19181 self._nativeEvents[name] = true; 19182 } 19183 } 19184 } 19185 19186 self.bindPendingEvents(); 19187 19188 if (settings.style) { 19189 elm = self.getEl(); 19190 if (elm) { 19191 elm.setAttribute('style', settings.style); 19192 elm.style.cssText = settings.style; 19193 } 19194 } 19195 19196 if (!self._visible) { 19197 DomUtils.css(self.getEl(), 'display', 'none'); 19198 } 19199 19200 if (self.settings.border) { 19201 box = self.borderBox(); 19202 DomUtils.css(self.getEl(), { 19203 'border-top-width': box.top, 19204 'border-right-width': box.right, 19205 'border-bottom-width': box.bottom, 19206 'border-left-width': box.left 19207 }); 19208 } 19209 19210 // Add instance to lookup 19211 var root = self.getRoot(); 19212 if (!root.controlIdLookup) { 19213 root.controlIdLookup = {}; 19214 } 19215 19216 root.controlIdLookup[self._id] = self; 19217 19218 for (var key in self._aria) { 19219 self.aria(key, self._aria[key]); 19220 } 19221 19222 self.fire('postrender', {}, false); 19223 }, 19224 19225 /** 19226 * Scrolls the current control into view. 19227 * 19228 * @method scrollIntoView 19229 * @param {String} align Alignment in view top|center|bottom. 19230 * @return {tinymce.ui.Control} Current control instance. 19231 */ 19232 scrollIntoView: function(align) { 19233 function getOffset(elm, rootElm) { 19234 var x, y, parent = elm; 19235 19236 x = y = 0; 19237 while (parent && parent != rootElm && parent.nodeType) { 19238 x += parent.offsetLeft || 0; 19239 y += parent.offsetTop || 0; 19240 parent = parent.offsetParent; 19241 } 19242 19243 return {x: x, y: y}; 19244 } 19245 19246 var elm = this.getEl(), parentElm = elm.parentNode; 19247 var x, y, width, height, parentWidth, parentHeight; 19248 var pos = getOffset(elm, parentElm); 19249 19250 x = pos.x; 19251 y = pos.y; 19252 width = elm.offsetWidth; 19253 height = elm.offsetHeight; 19254 parentWidth = parentElm.clientWidth; 19255 parentHeight = parentElm.clientHeight; 19256 19257 if (align == "end") { 19258 x -= parentWidth - width; 19259 y -= parentHeight - height; 19260 } else if (align == "center") { 19261 x -= (parentWidth / 2) - (width / 2); 19262 y -= (parentHeight / 2) - (height / 2); 19263 } 19264 19265 parentElm.scrollLeft = x; 19266 parentElm.scrollTop = y; 19267 19268 return this; 19269 }, 19270 19271 /** 19272 * Binds pending DOM events. 19273 * 19274 * @private 19275 */ 19276 bindPendingEvents: function() { 19277 var self = this, i, l, parents, eventRootCtrl, nativeEvents, name; 19278 19279 function delegate(e) { 19280 var control = self.getParentCtrl(e.target); 19281 19282 if (control) { 19283 control.fire(e.type, e); 19284 } 19285 } 19286 19287 function mouseLeaveHandler() { 19288 var ctrl = eventRootCtrl._lastHoverCtrl; 19289 19290 if (ctrl) { 19291 ctrl.fire("mouseleave", {target: ctrl.getEl()}); 19292 19293 ctrl.parents().each(function(ctrl) { 19294 ctrl.fire("mouseleave", {target: ctrl.getEl()}); 19295 }); 19296 19297 eventRootCtrl._lastHoverCtrl = null; 19298 } 19299 } 19300 19301 function mouseEnterHandler(e) { 19302 var ctrl = self.getParentCtrl(e.target), lastCtrl = eventRootCtrl._lastHoverCtrl, idx = 0, i, parents, lastParents; 19303 19304 // Over on a new control 19305 if (ctrl !== lastCtrl) { 19306 eventRootCtrl._lastHoverCtrl = ctrl; 19307 19308 parents = ctrl.parents().toArray().reverse(); 19309 parents.push(ctrl); 19310 19311 if (lastCtrl) { 19312 lastParents = lastCtrl.parents().toArray().reverse(); 19313 lastParents.push(lastCtrl); 19314 19315 for (idx = 0; idx < lastParents.length; idx++) { 19316 if (parents[idx] !== lastParents[idx]) { 19317 break; 19318 } 19319 } 19320 19321 for (i = lastParents.length - 1; i >= idx; i--) { 19322 lastCtrl = lastParents[i]; 19323 lastCtrl.fire("mouseleave", { 19324 target : lastCtrl.getEl() 19325 }); 19326 } 19327 } 19328 19329 for (i = idx; i < parents.length; i++) { 19330 ctrl = parents[i]; 19331 ctrl.fire("mouseenter", { 19332 target : ctrl.getEl() 19333 }); 19334 } 19335 } 19336 } 19337 19338 function fixWheelEvent(e) { 19339 e.preventDefault(); 19340 19341 if (e.type == "mousewheel") { 19342 e.deltaY = -1 / 40 * e.wheelDelta; 19343 19344 if (e.wheelDeltaX) { 19345 e.deltaX = -1 / 40 * e.wheelDeltaX; 19346 } 19347 } else { 19348 e.deltaX = 0; 19349 e.deltaY = e.detail; 19350 } 19351 19352 e = self.fire("wheel", e); 19353 } 19354 19355 self._rendered = true; 19356 19357 nativeEvents = self._nativeEvents; 19358 if (nativeEvents) { 19359 // Find event root element if it exists 19360 parents = self.parents().toArray(); 19361 parents.unshift(self); 19362 for (i = 0, l = parents.length; !eventRootCtrl && i < l; i++) { 19363 eventRootCtrl = parents[i]._eventsRoot; 19364 } 19365 19366 // Event root wasn't found the use the root control 19367 if (!eventRootCtrl) { 19368 eventRootCtrl = parents[parents.length - 1] || self; 19369 } 19370 19371 // Set the eventsRoot property on children that didn't have it 19372 self._eventsRoot = eventRootCtrl; 19373 for (l = i, i = 0; i < l; i++) { 19374 parents[i]._eventsRoot = eventRootCtrl; 19375 } 19376 19377 var eventRootDelegates = eventRootCtrl._delegates; 19378 if (!eventRootDelegates) { 19379 eventRootDelegates = eventRootCtrl._delegates = {}; 19380 } 19381 19382 // Bind native event delegates 19383 for (name in nativeEvents) { 19384 if (!nativeEvents) { 19385 return false; 19386 } 19387 19388 if (name === "wheel" && !hasWheelEventSupport) { 19389 if (hasMouseWheelEventSupport) { 19390 DomUtils.on(self.getEl(), "mousewheel", fixWheelEvent); 19391 } else { 19392 DomUtils.on(self.getEl(), "DOMMouseScroll", fixWheelEvent); 19393 } 19394 19395 continue; 19396 } 19397 19398 // Special treatment for mousenter/mouseleave since these doesn't bubble 19399 if (name === "mouseenter" || name === "mouseleave") { 19400 // Fake mousenter/mouseleave 19401 if (!eventRootCtrl._hasMouseEnter) { 19402 DomUtils.on(eventRootCtrl.getEl(), "mouseleave", mouseLeaveHandler); 19403 DomUtils.on(eventRootCtrl.getEl(), "mouseover", mouseEnterHandler); 19404 eventRootCtrl._hasMouseEnter = 1; 19405 } 19406 } else if (!eventRootDelegates[name]) { 19407 DomUtils.on(eventRootCtrl.getEl(), name, delegate); 19408 eventRootDelegates[name] = true; 19409 } 19410 19411 // Remove the event once it's bound 19412 nativeEvents[name] = false; 19413 } 19414 } 19415 }, 19416 19417 getRoot: function() { 19418 var ctrl = this, rootControl, parents = []; 19419 19420 while (ctrl) { 19421 if (ctrl.rootControl) { 19422 rootControl = ctrl.rootControl; 19423 break; 19424 } 19425 19426 parents.push(ctrl); 19427 rootControl = ctrl; 19428 ctrl = ctrl.parent(); 19429 } 19430 19431 if (!rootControl) { 19432 rootControl = this; 19433 } 19434 19435 var i = parents.length; 19436 while (i--) { 19437 parents[i].rootControl = rootControl; 19438 } 19439 19440 return rootControl; 19441 }, 19442 19443 /** 19444 * Reflows the current control and it's parents. 19445 * This should be used after you for example append children to the current control so 19446 * that the layout managers know that they need to reposition everything. 19447 * 19448 * @example 19449 * container.append({type: 'button', text: 'My button'}).reflow(); 19450 * 19451 * @method reflow 19452 * @return {tinymce.ui.Control} Current control instance. 19453 */ 19454 reflow: function() { 19455 this.repaint(); 19456 19457 return this; 19458 } 19459 19460 /** 19461 * Sets/gets the parent container for the control. 19462 * 19463 * @method parent 19464 * @param {tinymce.ui.Container} parent Optional parent to set. 19465 * @return {tinymce.ui.Control} Parent control or the current control on a set action. 19466 */ 19467 // parent: function(parent) {} -- Generated 19468 19469 /** 19470 * Sets/gets the text for the control. 19471 * 19472 * @method text 19473 * @param {String} value Value to set to control. 19474 * @return {String/tinymce.ui.Control} Current control on a set operation or current value on a get. 19475 */ 19476 // text: function(value) {} -- Generated 19477 19478 /** 19479 * Sets/gets the width for the control. 19480 * 19481 * @method width 19482 * @param {Number} value Value to set to control. 19483 * @return {Number/tinymce.ui.Control} Current control on a set operation or current value on a get. 19484 */ 19485 // width: function(value) {} -- Generated 19486 19487 /** 19488 * Sets/gets the height for the control. 19489 * 19490 * @method height 19491 * @param {Number} value Value to set to control. 19492 * @return {Number/tinymce.ui.Control} Current control on a set operation or current value on a get. 19493 */ 19494 // height: function(value) {} -- Generated 19495 19496 /** 19497 * Sets/gets the disabled state on the control. 19498 * 19499 * @method disabled 19500 * @param {Boolean} state Value to set to control. 19501 * @return {Boolean/tinymce.ui.Control} Current control on a set operation or current state on a get. 19502 */ 19503 // disabled: function(state) {} -- Generated 19504 19505 /** 19506 * Sets/gets the active for the control. 19507 * 19508 * @method active 19509 * @param {Boolean} state Value to set to control. 19510 * @return {Boolean/tinymce.ui.Control} Current control on a set operation or current state on a get. 19511 */ 19512 // active: function(state) {} -- Generated 19513 19514 /** 19515 * Sets/gets the name for the control. 19516 * 19517 * @method name 19518 * @param {String} value Value to set to control. 19519 * @return {String/tinymce.ui.Control} Current control on a set operation or current value on a get. 19520 */ 19521 // name: function(value) {} -- Generated 19522 19523 /** 19524 * Sets/gets the title for the control. 19525 * 19526 * @method title 19527 * @param {String} value Value to set to control. 19528 * @return {String/tinymce.ui.Control} Current control on a set operation or current value on a get. 19529 */ 19530 // title: function(value) {} -- Generated 19531 }); 19532 19533 return Control; 19534 }); 19535 19536 // Included from: js/tinymce/classes/ui/Factory.js 19537 19538 /** 19539 * Factory.js 19540 * 19541 * Copyright, Moxiecode Systems AB 19542 * Released under LGPL License. 19543 * 19544 * License: http://www.tinymce.com/license 19545 * Contributing: http://www.tinymce.com/contributing 19546 */ 19547 19548 /*global tinymce:true */ 19549 19550 /** 19551 * This class is a factory for control instances. This enables you 19552 * to create instances of controls without having to require the UI controls directly. 19553 * 19554 * It also allow you to override or add new control types. 19555 * 19556 * @class tinymce.ui.Factory 19557 */ 19558 define("tinymce/ui/Factory", [], function() { 19559 "use strict"; 19560 19561 var types = {}, namespaceInit; 19562 19563 return { 19564 /** 19565 * Adds a new control instance type to the factory. 19566 * 19567 * @method add 19568 * @param {String} type Type name for example "button". 19569 * @param {function} typeClass Class type function. 19570 */ 19571 add: function(type, typeClass) { 19572 types[type.toLowerCase()] = typeClass; 19573 }, 19574 19575 /** 19576 * Returns true/false if the specified type exists or not. 19577 * 19578 * @method has 19579 * @param {String} type Type to look for. 19580 * @return {Boolean} true/false if the control by name exists. 19581 */ 19582 has: function(type) { 19583 return !!types[type.toLowerCase()]; 19584 }, 19585 19586 /** 19587 * Creates a new control instance based on the settings provided. The instance created will be 19588 * based on the specified type property it can also create whole structures of components out of 19589 * the specified JSON object. 19590 * 19591 * @example 19592 * tinymce.ui.Factory.create({ 19593 * type: 'button', 19594 * text: 'Hello world!' 19595 * }); 19596 * 19597 * @method create 19598 * @param {Object/String} settings Name/Value object with items used to create the type. 19599 * @return {tinymce.ui.Control} Control instance based on the specified type. 19600 */ 19601 create: function(type, settings) { 19602 var ControlType, name, namespace; 19603 19604 // Build type lookup 19605 if (!namespaceInit) { 19606 namespace = tinymce.ui; 19607 19608 for (name in namespace) { 19609 types[name.toLowerCase()] = namespace[name]; 19610 } 19611 19612 namespaceInit = true; 19613 } 19614 19615 // If string is specified then use it as the type 19616 if (typeof(type) == 'string') { 19617 settings = settings || {}; 19618 settings.type = type; 19619 } else { 19620 settings = type; 19621 type = settings.type; 19622 } 19623 19624 // Find control type 19625 type = type.toLowerCase(); 19626 ControlType = types[type]; 19627 19628 // #if debug 19629 19630 if (!ControlType) { 19631 throw new Error("Could not find control by type: " + type); 19632 } 19633 19634 // #endif 19635 19636 ControlType = new ControlType(settings); 19637 ControlType.type = type; // Set the type on the instance, this will be used by the Selector engine 19638 19639 return ControlType; 19640 } 19641 }; 19642 }); 19643 19644 // Included from: js/tinymce/classes/ui/KeyboardNavigation.js 19645 19646 /** 19647 * KeyboardNavigation.js 19648 * 19649 * Copyright, Moxiecode Systems AB 19650 * Released under LGPL License. 19651 * 19652 * License: http://www.tinymce.com/license 19653 * Contributing: http://www.tinymce.com/contributing 19654 */ 19655 19656 /** 19657 * This class handles keyboard navigation of controls and elements. 19658 * 19659 * @class tinymce.ui.KeyboardNavigation 19660 */ 19661 define("tinymce/ui/KeyboardNavigation", [ 19662 ], function() { 19663 "use strict"; 19664 19665 /** 19666 * This class handles all keyboard navigation for WAI-ARIA support. Each root container 19667 * gets an instance of this class. 19668 * 19669 * @constructor 19670 */ 19671 return function(settings) { 19672 var root = settings.root, focusedElement, focusedControl; 19673 19674 focusedElement = document.activeElement; 19675 focusedControl = root.getParentCtrl(focusedElement); 19676 19677 /** 19678 * Returns the currently focused elements wai aria role of the currently 19679 * focused element or specified element. 19680 * 19681 * @private 19682 * @param {Element} elm Optional element to get role from. 19683 * @return {String} Role of specified element. 19684 */ 19685 function getRole(elm) { 19686 elm = elm || focusedElement; 19687 19688 return elm && elm.getAttribute('role'); 19689 } 19690 19691 /** 19692 * Returns the wai role of the parent element of the currently 19693 * focused element or specified element. 19694 * 19695 * @private 19696 * @param {Element} elm Optional element to get parent role from. 19697 * @return {String} Role of the first parent that has a role. 19698 */ 19699 function getParentRole(elm) { 19700 var role, parent = elm || focusedElement; 19701 19702 while ((parent = parent.parentNode)) { 19703 if ((role = getRole(parent))) { 19704 return role; 19705 } 19706 } 19707 } 19708 19709 /** 19710 * Returns a wai aria property by name for example aria-selected. 19711 * 19712 * @private 19713 * @param {String} name Name of the aria property to get for example "disabled". 19714 * @return {String} Aria property value. 19715 */ 19716 function getAriaProp(name) { 19717 var elm = focusedElement; 19718 19719 if (elm) { 19720 return elm.getAttribute('aria-' + name); 19721 } 19722 } 19723 19724 /** 19725 * Is the element a text input element or not. 19726 * 19727 * @private 19728 * @param {Element} elm Element to check if it's an text input element or not. 19729 * @return {Boolean} True/false if the element is a text element or not. 19730 */ 19731 function isTextInputElement(elm) { 19732 var tagName = elm.tagName.toUpperCase(); 19733 19734 // Notice: since type can be "email" etc we don't check the type 19735 // So all input elements gets treated as text input elements 19736 return tagName == "INPUT" || tagName == "TEXTAREA"; 19737 } 19738 19739 /** 19740 * Returns true/false if the specified element can be focused or not. 19741 * 19742 * @private 19743 * @param {Element} elm DOM element to check if it can be focused or not. 19744 * @return {Boolean} True/false if the element can have focus. 19745 */ 19746 function canFocus(elm) { 19747 if (isTextInputElement(elm) && !elm.hidden) { 19748 return true; 19749 } 19750 19751 if (/^(button|menuitem|checkbox|tab|menuitemcheckbox|option|gridcell)$/.test(getRole(elm))) { 19752 return true; 19753 } 19754 19755 return false; 19756 } 19757 19758 /** 19759 * Returns an array of focusable visible elements within the specified container element. 19760 * 19761 * @private 19762 * @param {Element} elm DOM element to find focusable elements within. 19763 * @return {Array} Array of focusable elements. 19764 */ 19765 function getFocusElements(elm) { 19766 var elements = []; 19767 19768 function collect(elm) { 19769 if (elm.nodeType != 1 || elm.style.display == 'none') { 19770 return; 19771 } 19772 19773 if (canFocus(elm)) { 19774 elements.push(elm); 19775 } 19776 19777 for (var i = 0; i < elm.childNodes.length; i++) { 19778 collect(elm.childNodes[i]); 19779 } 19780 } 19781 19782 collect(elm || root.getEl()); 19783 19784 return elements; 19785 } 19786 19787 /** 19788 * Returns the navigation root control for the specified control. The navigation root 19789 * is the control that the keyboard navigation gets scoped to for example a menubar or toolbar group. 19790 * It will look for parents of the specified target control or the currenty focused control if this option is omitted. 19791 * 19792 * @private 19793 * @param {tinymce.ui.Control} targetControl Optional target control to find root of. 19794 * @return {tinymce.ui.Control} Navigation root control. 19795 */ 19796 function getNavigationRoot(targetControl) { 19797 var navigationRoot, controls; 19798 19799 targetControl = targetControl || focusedControl; 19800 controls = targetControl.parents().toArray(); 19801 controls.unshift(targetControl); 19802 19803 for (var i = 0; i < controls.length; i++) { 19804 navigationRoot = controls[i]; 19805 19806 if (navigationRoot.settings.ariaRoot) { 19807 break; 19808 } 19809 } 19810 19811 return navigationRoot; 19812 } 19813 19814 /** 19815 * Focuses the first item in the specified targetControl element or the last aria index if the 19816 * navigation root has the ariaRemember option enabled. 19817 * 19818 * @private 19819 * @param {tinymce.ui.Control} targetControl Target control to focus the first item in. 19820 */ 19821 function focusFirst(targetControl) { 19822 var navigationRoot = getNavigationRoot(targetControl); 19823 var focusElements = getFocusElements(navigationRoot.getEl()); 19824 19825 if (navigationRoot.settings.ariaRemember && "lastAriaIndex" in navigationRoot) { 19826 moveFocusToIndex(navigationRoot.lastAriaIndex, focusElements); 19827 } else { 19828 moveFocusToIndex(0, focusElements); 19829 } 19830 } 19831 19832 /** 19833 * Moves the focus to the specified index within the elements list. 19834 * This will scope the index to the size of the element list if it changed. 19835 * 19836 * @private 19837 * @param {Number} idx Specified index to move to. 19838 * @param {Array} elements Array with dom elements to move focus within. 19839 * @return {Number} Input index or a changed index if it was out of range. 19840 */ 19841 function moveFocusToIndex(idx, elements) { 19842 if (idx < 0) { 19843 idx = elements.length - 1; 19844 } else if (idx >= elements.length) { 19845 idx = 0; 19846 } 19847 19848 if (elements[idx]) { 19849 elements[idx].focus(); 19850 } 19851 19852 return idx; 19853 } 19854 19855 /** 19856 * Moves the focus forwards or backwards. 19857 * 19858 * @private 19859 * @param {Number} dir Direction to move in positive means forward, negative means backwards. 19860 * @param {Array} elements Optional array of elements to move within defaults to the current navigation roots elements. 19861 */ 19862 function moveFocus(dir, elements) { 19863 var idx = -1, navigationRoot = getNavigationRoot(); 19864 19865 elements = elements || getFocusElements(navigationRoot.getEl()); 19866 19867 for (var i = 0; i < elements.length; i++) { 19868 if (elements[i] === focusedElement) { 19869 idx = i; 19870 } 19871 } 19872 19873 idx += dir; 19874 navigationRoot.lastAriaIndex = moveFocusToIndex(idx, elements); 19875 } 19876 19877 /** 19878 * Moves the focus to the left this is called by the left key. 19879 * 19880 * @private 19881 */ 19882 function left() { 19883 var parentRole = getParentRole(); 19884 19885 if (parentRole == "tablist") { 19886 moveFocus(-1, getFocusElements(focusedElement.parentNode)); 19887 } else if (focusedControl.parent().submenu) { 19888 cancel(); 19889 } else { 19890 moveFocus(-1); 19891 } 19892 } 19893 19894 /** 19895 * Moves the focus to the right this is called by the right key. 19896 * 19897 * @private 19898 */ 19899 function right() { 19900 var role = getRole(), parentRole = getParentRole(); 19901 19902 if (parentRole == "tablist") { 19903 moveFocus(1, getFocusElements(focusedElement.parentNode)); 19904 } else if (role == "menuitem" && parentRole == "menu" && getAriaProp('haspopup')) { 19905 enter(); 19906 } else { 19907 moveFocus(1); 19908 } 19909 } 19910 19911 /** 19912 * Moves the focus to the up this is called by the up key. 19913 * 19914 * @private 19915 */ 19916 function up() { 19917 moveFocus(-1); 19918 } 19919 19920 /** 19921 * Moves the focus to the up this is called by the down key. 19922 * 19923 * @private 19924 */ 19925 function down() { 19926 var role = getRole(), parentRole = getParentRole(); 19927 19928 if (role == "menuitem" && parentRole == "menubar") { 19929 enter(); 19930 } else if (role == "button" && getAriaProp('haspopup')) { 19931 enter({key: 'down'}); 19932 } else { 19933 moveFocus(1); 19934 } 19935 } 19936 19937 /** 19938 * Moves the focus to the next item or previous item depending on shift key. 19939 * 19940 * @private 19941 * @param {DOMEvent} e DOM event object. 19942 */ 19943 function tab(e) { 19944 var parentRole = getParentRole(); 19945 19946 if (parentRole == "tablist") { 19947 var elm = getFocusElements(focusedControl.getEl('body'))[0]; 19948 19949 if (elm) { 19950 elm.focus(); 19951 } 19952 } else { 19953 moveFocus(e.shiftKey ? -1 : 1); 19954 } 19955 } 19956 19957 /** 19958 * Calls the cancel event on the currently focused control. This is normally done using the Esc key. 19959 * 19960 * @private 19961 */ 19962 function cancel() { 19963 focusedControl.fire('cancel'); 19964 } 19965 19966 /** 19967 * Calls the click event on the currently focused control. This is normally done using the Enter/Space keys. 19968 * 19969 * @private 19970 * @param {Object} aria Optional aria data to pass along with the enter event. 19971 */ 19972 function enter(aria) { 19973 aria = aria || {}; 19974 focusedControl.fire('click', {target: focusedElement, aria: aria}); 19975 } 19976 19977 root.on('keydown', function(e) { 19978 function handleNonTabOrEscEvent(e, handler) { 19979 // Ignore non tab keys for text elements 19980 if (isTextInputElement(focusedElement)) { 19981 return; 19982 } 19983 19984 if (handler(e) !== false) { 19985 e.preventDefault(); 19986 } 19987 } 19988 19989 if (e.isDefaultPrevented()) { 19990 return; 19991 } 19992 19993 switch (e.keyCode) { 19994 case 37: // DOM_VK_LEFT 19995 handleNonTabOrEscEvent(e, left); 19996 break; 19997 19998 case 39: // DOM_VK_RIGHT 19999 handleNonTabOrEscEvent(e, right); 20000 break; 20001 20002 case 38: // DOM_VK_UP 20003 handleNonTabOrEscEvent(e, up); 20004 break; 20005 20006 case 40: // DOM_VK_DOWN 20007 handleNonTabOrEscEvent(e, down); 20008 break; 20009 20010 case 27: // DOM_VK_ESCAPE 20011 cancel(); 20012 break; 20013 20014 case 14: // DOM_VK_ENTER 20015 case 13: // DOM_VK_RETURN 20016 case 32: // DOM_VK_SPACE 20017 handleNonTabOrEscEvent(e, enter); 20018 break; 20019 20020 case 9: // DOM_VK_TAB 20021 if (tab(e) !== false) { 20022 e.preventDefault(); 20023 } 20024 break; 20025 } 20026 }); 20027 20028 root.on('focusin', function(e) { 20029 focusedElement = e.target; 20030 focusedControl = e.control; 20031 }); 20032 20033 return { 20034 focusFirst: focusFirst 20035 }; 20036 }; 20037 }); 20038 20039 // Included from: js/tinymce/classes/ui/Container.js 20040 20041 /** 20042 * Container.js 20043 * 20044 * Copyright, Moxiecode Systems AB 20045 * Released under LGPL License. 20046 * 20047 * License: http://www.tinymce.com/license 20048 * Contributing: http://www.tinymce.com/contributing 20049 */ 20050 20051 /** 20052 * Container control. This is extended by all controls that can have 20053 * children such as panels etc. You can also use this class directly as an 20054 * generic container instance. The container doesn't have any specific role or style. 20055 * 20056 * @-x-less Container.less 20057 * @class tinymce.ui.Container 20058 * @extends tinymce.ui.Control 20059 */ 20060 define("tinymce/ui/Container", [ 20061 "tinymce/ui/Control", 20062 "tinymce/ui/Collection", 20063 "tinymce/ui/Selector", 20064 "tinymce/ui/Factory", 20065 "tinymce/ui/KeyboardNavigation", 20066 "tinymce/util/Tools", 20067 "tinymce/ui/DomUtils" 20068 ], function(Control, Collection, Selector, Factory, KeyboardNavigation, Tools, DomUtils) { 20069 "use strict"; 20070 20071 var selectorCache = {}; 20072 20073 return Control.extend({ 20074 layout: '', 20075 innerClass: 'container-inner', 20076 20077 /** 20078 * Constructs a new control instance with the specified settings. 20079 * 20080 * @constructor 20081 * @param {Object} settings Name/value object with settings. 20082 * @setting {Array} items Items to add to container in JSON format or control instances. 20083 * @setting {String} layout Layout manager by name to use. 20084 * @setting {Object} defaults Default settings to apply to all items. 20085 */ 20086 init: function(settings) { 20087 var self = this; 20088 20089 self._super(settings); 20090 settings = self.settings; 20091 self._fixed = settings.fixed; 20092 self._items = new Collection(); 20093 20094 if (self.isRtl()) { 20095 self.addClass('rtl'); 20096 } 20097 20098 self.addClass('container'); 20099 self.addClass('container-body', 'body'); 20100 20101 if (settings.containerCls) { 20102 self.addClass(settings.containerCls); 20103 } 20104 20105 self._layout = Factory.create((settings.layout || self.layout) + 'layout'); 20106 20107 if (self.settings.items) { 20108 self.add(self.settings.items); 20109 } 20110 20111 // TODO: Fix this! 20112 self._hasBody = true; 20113 }, 20114 20115 /** 20116 * Returns a collection of child items that the container currently have. 20117 * 20118 * @method items 20119 * @return {tinymce.ui.Collection} Control collection direct child controls. 20120 */ 20121 items: function() { 20122 return this._items; 20123 }, 20124 20125 /** 20126 * Find child controls by selector. 20127 * 20128 * @method find 20129 * @param {String} selector Selector CSS pattern to find children by. 20130 * @return {tinymce.ui.Collection} Control collection with child controls. 20131 */ 20132 find: function(selector) { 20133 selector = selectorCache[selector] = selectorCache[selector] || new Selector(selector); 20134 20135 return selector.find(this); 20136 }, 20137 20138 /** 20139 * Adds one or many items to the current container. This will create instances of 20140 * the object representations if needed. 20141 * 20142 * @method add 20143 * @param {Array/Object/tinymce.ui.Control} items Array or item that will be added to the container. 20144 * @return {tinymce.ui.Collection} Current collection control. 20145 */ 20146 add: function(items) { 20147 var self = this; 20148 20149 self.items().add(self.create(items)).parent(self); 20150 20151 return self; 20152 }, 20153 20154 /** 20155 * Focuses the current container instance. This will look 20156 * for the first control in the container and focus that. 20157 * 20158 * @method focus 20159 * @param {Boolean} keyboard Optional true/false if the focus was a keyboard focus or not. 20160 * @return {tinymce.ui.Collection} Current instance. 20161 */ 20162 focus: function(keyboard) { 20163 var self = this, focusCtrl, keyboardNav, items; 20164 20165 if (keyboard) { 20166 keyboardNav = self.keyboardNav || self.parents().eq(-1)[0].keyboardNav; 20167 20168 if (keyboardNav) { 20169 keyboardNav.focusFirst(self); 20170 return; 20171 } 20172 } 20173 20174 items = self.find('*'); 20175 20176 // TODO: Figure out a better way to auto focus alert dialog buttons 20177 if (self.statusbar) { 20178 items.add(self.statusbar.items()); 20179 } 20180 20181 items.each(function(ctrl) { 20182 if (ctrl.settings.autofocus) { 20183 focusCtrl = null; 20184 return false; 20185 } 20186 20187 if (ctrl.canFocus) { 20188 focusCtrl = focusCtrl || ctrl; 20189 } 20190 }); 20191 20192 if (focusCtrl) { 20193 focusCtrl.focus(); 20194 } 20195 20196 return self; 20197 }, 20198 20199 /** 20200 * Replaces the specified child control with a new control. 20201 * 20202 * @method replace 20203 * @param {tinymce.ui.Control} oldItem Old item to be replaced. 20204 * @param {tinymce.ui.Control} newItem New item to be inserted. 20205 */ 20206 replace: function(oldItem, newItem) { 20207 var ctrlElm, items = this.items(), i = items.length; 20208 20209 // Replace the item in collection 20210 while (i--) { 20211 if (items[i] === oldItem) { 20212 items[i] = newItem; 20213 break; 20214 } 20215 } 20216 20217 if (i >= 0) { 20218 // Remove new item from DOM 20219 ctrlElm = newItem.getEl(); 20220 if (ctrlElm) { 20221 ctrlElm.parentNode.removeChild(ctrlElm); 20222 } 20223 20224 // Remove old item from DOM 20225 ctrlElm = oldItem.getEl(); 20226 if (ctrlElm) { 20227 ctrlElm.parentNode.removeChild(ctrlElm); 20228 } 20229 } 20230 20231 // Adopt the item 20232 newItem.parent(this); 20233 }, 20234 20235 /** 20236 * Creates the specified items. If any of the items is plain JSON style objects 20237 * it will convert these into real tinymce.ui.Control instances. 20238 * 20239 * @method create 20240 * @param {Array} items Array of items to convert into control instances. 20241 * @return {Array} Array with control instances. 20242 */ 20243 create: function(items) { 20244 var self = this, settings, ctrlItems = []; 20245 20246 // Non array structure, then force it into an array 20247 if (!Tools.isArray(items)) { 20248 items = [items]; 20249 } 20250 20251 // Add default type to each child control 20252 Tools.each(items, function(item) { 20253 if (item) { 20254 // Construct item if needed 20255 if (!(item instanceof Control)) { 20256 // Name only then convert it to an object 20257 if (typeof(item) == "string") { 20258 item = {type: item}; 20259 } 20260 20261 // Create control instance based on input settings and default settings 20262 settings = Tools.extend({}, self.settings.defaults, item); 20263 item.type = settings.type = settings.type || item.type || self.settings.defaultType || 20264 (settings.defaults ? settings.defaults.type : null); 20265 item = Factory.create(settings); 20266 } 20267 20268 ctrlItems.push(item); 20269 } 20270 }); 20271 20272 return ctrlItems; 20273 }, 20274 20275 /** 20276 * Renders new control instances. 20277 * 20278 * @private 20279 */ 20280 renderNew: function() { 20281 var self = this; 20282 20283 // Render any new items 20284 self.items().each(function(ctrl, index) { 20285 var containerElm, fragment; 20286 20287 ctrl.parent(self); 20288 20289 if (!ctrl._rendered) { 20290 containerElm = self.getEl('body'); 20291 fragment = DomUtils.createFragment(ctrl.renderHtml()); 20292 20293 // Insert or append the item 20294 if (containerElm.hasChildNodes() && index <= containerElm.childNodes.length - 1) { 20295 containerElm.insertBefore(fragment, containerElm.childNodes[index]); 20296 } else { 20297 containerElm.appendChild(fragment); 20298 } 20299 20300 ctrl.postRender(); 20301 } 20302 }); 20303 20304 self._layout.applyClasses(self); 20305 self._lastRect = null; 20306 20307 return self; 20308 }, 20309 20310 /** 20311 * Appends new instances to the current container. 20312 * 20313 * @method append 20314 * @param {Array/tinymce.ui.Collection} items Array if controls to append. 20315 * @return {tinymce.ui.Container} Current container instance. 20316 */ 20317 append: function(items) { 20318 return this.add(items).renderNew(); 20319 }, 20320 20321 /** 20322 * Prepends new instances to the current container. 20323 * 20324 * @method prepend 20325 * @param {Array/tinymce.ui.Collection} items Array if controls to prepend. 20326 * @return {tinymce.ui.Container} Current container instance. 20327 */ 20328 prepend: function(items) { 20329 var self = this; 20330 20331 self.items().set(self.create(items).concat(self.items().toArray())); 20332 20333 return self.renderNew(); 20334 }, 20335 20336 /** 20337 * Inserts an control at a specific index. 20338 * 20339 * @method insert 20340 * @param {Array/tinymce.ui.Collection} items Array if controls to insert. 20341 * @param {Number} index Index to insert controls at. 20342 * @param {Boolean} [before=false] Inserts controls before the index. 20343 */ 20344 insert: function(items, index, before) { 20345 var self = this, curItems, beforeItems, afterItems; 20346 20347 items = self.create(items); 20348 curItems = self.items(); 20349 20350 if (!before && index < curItems.length - 1) { 20351 index += 1; 20352 } 20353 20354 if (index >= 0 && index < curItems.length) { 20355 beforeItems = curItems.slice(0, index).toArray(); 20356 afterItems = curItems.slice(index).toArray(); 20357 curItems.set(beforeItems.concat(items, afterItems)); 20358 } 20359 20360 return self.renderNew(); 20361 }, 20362 20363 /** 20364 * Populates the form fields from the specified JSON data object. 20365 * 20366 * Control items in the form that matches the data will have it's value set. 20367 * 20368 * @method fromJSON 20369 * @param {Object} data JSON data object to set control values by. 20370 * @return {tinymce.ui.Container} Current form instance. 20371 */ 20372 fromJSON: function(data) { 20373 var self = this; 20374 20375 for (var name in data) { 20376 self.find('#' + name).value(data[name]); 20377 } 20378 20379 return self; 20380 }, 20381 20382 /** 20383 * Serializes the form into a JSON object by getting all items 20384 * that has a name and a value. 20385 * 20386 * @method toJSON 20387 * @return {Object} JSON object with form data. 20388 */ 20389 toJSON: function() { 20390 var self = this, data = {}; 20391 20392 self.find('*').each(function(ctrl) { 20393 var name = ctrl.name(), value = ctrl.value(); 20394 20395 if (name && typeof(value) != "undefined") { 20396 data[name] = value; 20397 } 20398 }); 20399 20400 return data; 20401 }, 20402 20403 preRender: function() { 20404 }, 20405 20406 /** 20407 * Renders the control as a HTML string. 20408 * 20409 * @method renderHtml 20410 * @return {String} HTML representing the control. 20411 */ 20412 renderHtml: function() { 20413 var self = this, layout = self._layout, role = this.settings.role; 20414 20415 self.preRender(); 20416 layout.preRender(self); 20417 20418 return ( 20419 '<div id="' + self._id + '" class="' + self.classes() + '"' + (role ? ' role="' + this.settings.role + '"' : '') + '>' + 20420 '<div id="' + self._id + '-body" class="' + self.classes('body') + '">' + 20421 (self.settings.html || '') + layout.renderHtml(self) + 20422 '</div>' + 20423 '</div>' 20424 ); 20425 }, 20426 20427 /** 20428 * Post render method. Called after the control has been rendered to the target. 20429 * 20430 * @method postRender 20431 * @return {tinymce.ui.Container} Current combobox instance. 20432 */ 20433 postRender: function() { 20434 var self = this, box; 20435 20436 self.items().exec('postRender'); 20437 self._super(); 20438 20439 self._layout.postRender(self); 20440 self._rendered = true; 20441 20442 if (self.settings.style) { 20443 DomUtils.css(self.getEl(), self.settings.style); 20444 } 20445 20446 if (self.settings.border) { 20447 box = self.borderBox(); 20448 DomUtils.css(self.getEl(), { 20449 'border-top-width': box.top, 20450 'border-right-width': box.right, 20451 'border-bottom-width': box.bottom, 20452 'border-left-width': box.left 20453 }); 20454 } 20455 20456 if (!self.parent()) { 20457 self.keyboardNav = new KeyboardNavigation({ 20458 root: self 20459 }); 20460 } 20461 20462 return self; 20463 }, 20464 20465 /** 20466 * Initializes the current controls layout rect. 20467 * This will be executed by the layout managers to determine the 20468 * default minWidth/minHeight etc. 20469 * 20470 * @method initLayoutRect 20471 * @return {Object} Layout rect instance. 20472 */ 20473 initLayoutRect: function() { 20474 var self = this, layoutRect = self._super(); 20475 20476 // Recalc container size by asking layout manager 20477 self._layout.recalc(self); 20478 20479 return layoutRect; 20480 }, 20481 20482 /** 20483 * Recalculates the positions of the controls in the current container. 20484 * This is invoked by the reflow method and shouldn't be called directly. 20485 * 20486 * @method recalc 20487 */ 20488 recalc: function() { 20489 var self = this, rect = self._layoutRect, lastRect = self._lastRect; 20490 20491 if (!lastRect || lastRect.w != rect.w || lastRect.h != rect.h) { 20492 self._layout.recalc(self); 20493 rect = self.layoutRect(); 20494 self._lastRect = {x: rect.x, y: rect.y, w: rect.w, h: rect.h}; 20495 return true; 20496 } 20497 }, 20498 20499 /** 20500 * Reflows the current container and it's children and possible parents. 20501 * This should be used after you for example append children to the current control so 20502 * that the layout managers know that they need to reposition everything. 20503 * 20504 * @example 20505 * container.append({type: 'button', text: 'My button'}).reflow(); 20506 * 20507 * @method reflow 20508 * @return {tinymce.ui.Container} Current container instance. 20509 */ 20510 reflow: function() { 20511 var i; 20512 20513 if (this.visible()) { 20514 Control.repaintControls = []; 20515 Control.repaintControls.map = {}; 20516 20517 this.recalc(); 20518 i = Control.repaintControls.length; 20519 20520 while (i--) { 20521 Control.repaintControls[i].repaint(); 20522 } 20523 20524 // TODO: Fix me! 20525 if (this.settings.layout !== "flow" && this.settings.layout !== "stack") { 20526 this.repaint(); 20527 } 20528 20529 Control.repaintControls = []; 20530 } 20531 20532 return this; 20533 } 20534 }); 20535 }); 20536 20537 // Included from: js/tinymce/classes/ui/DragHelper.js 20538 20539 /** 20540 * DragHelper.js 20541 * 20542 * Copyright, Moxiecode Systems AB 20543 * Released under LGPL License. 20544 * 20545 * License: http://www.tinymce.com/license 20546 * Contributing: http://www.tinymce.com/contributing 20547 */ 20548 20549 /** 20550 * Drag/drop helper class. 20551 * 20552 * @example 20553 * var dragHelper = new tinymce.ui.DragHelper('mydiv', { 20554 * start: function(evt) { 20555 * }, 20556 * 20557 * drag: function(evt) { 20558 * }, 20559 * 20560 * end: function(evt) { 20561 * } 20562 * }); 20563 * 20564 * @class tinymce.ui.DragHelper 20565 */ 20566 define("tinymce/ui/DragHelper", [ 20567 "tinymce/ui/DomUtils" 20568 ], function(DomUtils) { 20569 "use strict"; 20570 20571 function getDocumentSize() { 20572 var doc = document, documentElement, body, scrollWidth, clientWidth; 20573 var offsetWidth, scrollHeight, clientHeight, offsetHeight, max = Math.max; 20574 20575 documentElement = doc.documentElement; 20576 body = doc.body; 20577 20578 scrollWidth = max(documentElement.scrollWidth, body.scrollWidth); 20579 clientWidth = max(documentElement.clientWidth, body.clientWidth); 20580 offsetWidth = max(documentElement.offsetWidth, body.offsetWidth); 20581 20582 scrollHeight = max(documentElement.scrollHeight, body.scrollHeight); 20583 clientHeight = max(documentElement.clientHeight, body.clientHeight); 20584 offsetHeight = max(documentElement.offsetHeight, body.offsetHeight); 20585 20586 return { 20587 width: scrollWidth < offsetWidth ? clientWidth : scrollWidth, 20588 height: scrollHeight < offsetHeight ? clientHeight : scrollHeight 20589 }; 20590 } 20591 20592 return function(id, settings) { 20593 var eventOverlayElm, doc = document, downButton, start, stop, drag, startX, startY; 20594 20595 settings = settings || {}; 20596 20597 function getHandleElm() { 20598 return doc.getElementById(settings.handle || id); 20599 } 20600 20601 start = function(e) { 20602 var docSize = getDocumentSize(), handleElm, cursor; 20603 20604 e.preventDefault(); 20605 downButton = e.button; 20606 handleElm = getHandleElm(); 20607 startX = e.screenX; 20608 startY = e.screenY; 20609 20610 // Grab cursor from handle 20611 if (window.getComputedStyle) { 20612 cursor = window.getComputedStyle(handleElm, null).getPropertyValue("cursor"); 20613 } else { 20614 cursor = handleElm.runtimeStyle.cursor; 20615 } 20616 20617 // Create event overlay and add it to document 20618 eventOverlayElm = doc.createElement('div'); 20619 DomUtils.css(eventOverlayElm, { 20620 position: "absolute", 20621 top: 0, left: 0, 20622 width: docSize.width, 20623 height: docSize.height, 20624 zIndex: 0x7FFFFFFF, 20625 opacity: 0.0001, 20626 background: 'red', 20627 cursor: cursor 20628 }); 20629 20630 doc.body.appendChild(eventOverlayElm); 20631 20632 // Bind mouse events 20633 DomUtils.on(doc, 'mousemove', drag); 20634 DomUtils.on(doc, 'mouseup', stop); 20635 20636 // Begin drag 20637 settings.start(e); 20638 }; 20639 20640 drag = function(e) { 20641 if (e.button !== downButton) { 20642 return stop(e); 20643 } 20644 20645 e.deltaX = e.screenX - startX; 20646 e.deltaY = e.screenY - startY; 20647 20648 e.preventDefault(); 20649 settings.drag(e); 20650 }; 20651 20652 stop = function(e) { 20653 DomUtils.off(doc, 'mousemove', drag); 20654 DomUtils.off(doc, 'mouseup', stop); 20655 20656 eventOverlayElm.parentNode.removeChild(eventOverlayElm); 20657 20658 if (settings.stop) { 20659 settings.stop(e); 20660 } 20661 }; 20662 20663 /** 20664 * Destroys the drag/drop helper instance. 20665 * 20666 * @method destroy 20667 */ 20668 this.destroy = function() { 20669 DomUtils.off(getHandleElm()); 20670 }; 20671 20672 DomUtils.on(getHandleElm(), 'mousedown', start); 20673 }; 20674 }); 20675 20676 // Included from: js/tinymce/classes/ui/Scrollable.js 20677 20678 /** 20679 * Scrollable.js 20680 * 20681 * Copyright, Moxiecode Systems AB 20682 * Released under LGPL License. 20683 * 20684 * License: http://www.tinymce.com/license 20685 * Contributing: http://www.tinymce.com/contributing 20686 */ 20687 20688 /** 20689 * This mixin makes controls scrollable using custom scrollbars. 20690 * 20691 * @-x-less Scrollable.less 20692 * @mixin tinymce.ui.Scrollable 20693 */ 20694 define("tinymce/ui/Scrollable", [ 20695 "tinymce/ui/DomUtils", 20696 "tinymce/ui/DragHelper" 20697 ], function(DomUtils, DragHelper) { 20698 "use strict"; 20699 20700 return { 20701 init: function() { 20702 var self = this; 20703 self.on('repaint', self.renderScroll); 20704 }, 20705 20706 renderScroll: function() { 20707 var self = this, margin = 2; 20708 20709 function repaintScroll() { 20710 var hasScrollH, hasScrollV, bodyElm; 20711 20712 function repaintAxis(axisName, posName, sizeName, contentSizeName, hasScroll, ax) { 20713 var containerElm, scrollBarElm, scrollThumbElm; 20714 var containerSize, scrollSize, ratio, rect; 20715 var posNameLower, sizeNameLower; 20716 20717 scrollBarElm = self.getEl('scroll' + axisName); 20718 if (scrollBarElm) { 20719 posNameLower = posName.toLowerCase(); 20720 sizeNameLower = sizeName.toLowerCase(); 20721 20722 if (self.getEl('absend')) { 20723 DomUtils.css(self.getEl('absend'), posNameLower, self.layoutRect()[contentSizeName] - 1); 20724 } 20725 20726 if (!hasScroll) { 20727 DomUtils.css(scrollBarElm, 'display', 'none'); 20728 return; 20729 } 20730 20731 DomUtils.css(scrollBarElm, 'display', 'block'); 20732 containerElm = self.getEl('body'); 20733 scrollThumbElm = self.getEl('scroll' + axisName + "t"); 20734 containerSize = containerElm["client" + sizeName] - (margin * 2); 20735 containerSize -= hasScrollH && hasScrollV ? scrollBarElm["client" + ax] : 0; 20736 scrollSize = containerElm["scroll" + sizeName]; 20737 ratio = containerSize / scrollSize; 20738 20739 rect = {}; 20740 rect[posNameLower] = containerElm["offset" + posName] + margin; 20741 rect[sizeNameLower] = containerSize; 20742 DomUtils.css(scrollBarElm, rect); 20743 20744 rect = {}; 20745 rect[posNameLower] = containerElm["scroll" + posName] * ratio; 20746 rect[sizeNameLower] = containerSize * ratio; 20747 DomUtils.css(scrollThumbElm, rect); 20748 } 20749 } 20750 20751 bodyElm = self.getEl('body'); 20752 hasScrollH = bodyElm.scrollWidth > bodyElm.clientWidth; 20753 hasScrollV = bodyElm.scrollHeight > bodyElm.clientHeight; 20754 20755 repaintAxis("h", "Left", "Width", "contentW", hasScrollH, "Height"); 20756 repaintAxis("v", "Top", "Height", "contentH", hasScrollV, "Width"); 20757 } 20758 20759 function addScroll() { 20760 function addScrollAxis(axisName, posName, sizeName, deltaPosName, ax) { 20761 var scrollStart, axisId = self._id + '-scroll' + axisName, prefix = self.classPrefix; 20762 20763 self.getEl().appendChild(DomUtils.createFragment( 20764 '<div id="' + axisId + '" class="' + prefix + 'scrollbar ' + prefix + 'scrollbar-' + axisName + '">' + 20765 '<div id="' + axisId + 't" class="' + prefix + 'scrollbar-thumb"></div>' + 20766 '</div>' 20767 )); 20768 20769 self.draghelper = new DragHelper(axisId + 't', { 20770 start: function() { 20771 scrollStart = self.getEl('body')["scroll" + posName]; 20772 DomUtils.addClass(DomUtils.get(axisId), prefix + 'active'); 20773 }, 20774 20775 drag: function(e) { 20776 var ratio, hasScrollH, hasScrollV, containerSize, layoutRect = self.layoutRect(); 20777 20778 hasScrollH = layoutRect.contentW > layoutRect.innerW; 20779 hasScrollV = layoutRect.contentH > layoutRect.innerH; 20780 containerSize = self.getEl('body')["client" + sizeName] - (margin * 2); 20781 containerSize -= hasScrollH && hasScrollV ? self.getEl('scroll' + axisName)["client" + ax] : 0; 20782 20783 ratio = containerSize / self.getEl('body')["scroll" + sizeName]; 20784 self.getEl('body')["scroll" + posName] = scrollStart + (e["delta" + deltaPosName] / ratio); 20785 }, 20786 20787 stop: function() { 20788 DomUtils.removeClass(DomUtils.get(axisId), prefix + 'active'); 20789 } 20790 }); 20791 /* 20792 self.on('click', function(e) { 20793 if (e.target.id == self._id + '-scrollv') { 20794 20795 } 20796 });*/ 20797 } 20798 20799 self.addClass('scroll'); 20800 20801 addScrollAxis("v", "Top", "Height", "Y", "Width"); 20802 addScrollAxis("h", "Left", "Width", "X", "Height"); 20803 } 20804 20805 if (self.settings.autoScroll) { 20806 if (!self._hasScroll) { 20807 self._hasScroll = true; 20808 addScroll(); 20809 20810 self.on('wheel', function(e) { 20811 var bodyEl = self.getEl('body'); 20812 20813 bodyEl.scrollLeft += (e.deltaX || 0) * 10; 20814 bodyEl.scrollTop += e.deltaY * 10; 20815 20816 repaintScroll(); 20817 }); 20818 20819 DomUtils.on(self.getEl('body'), "scroll", repaintScroll); 20820 } 20821 20822 repaintScroll(); 20823 } 20824 } 20825 }; 20826 }); 20827 20828 // Included from: js/tinymce/classes/ui/Panel.js 20829 20830 /** 20831 * Panel.js 20832 * 20833 * Copyright, Moxiecode Systems AB 20834 * Released under LGPL License. 20835 * 20836 * License: http://www.tinymce.com/license 20837 * Contributing: http://www.tinymce.com/contributing 20838 */ 20839 20840 /** 20841 * Creates a new panel. 20842 * 20843 * @-x-less Panel.less 20844 * @class tinymce.ui.Panel 20845 * @extends tinymce.ui.Container 20846 * @mixes tinymce.ui.Scrollable 20847 */ 20848 define("tinymce/ui/Panel", [ 20849 "tinymce/ui/Container", 20850 "tinymce/ui/Scrollable" 20851 ], function(Container, Scrollable) { 20852 "use strict"; 20853 20854 return Container.extend({ 20855 Defaults: { 20856 layout: 'fit', 20857 containerCls: 'panel' 20858 }, 20859 20860 Mixins: [Scrollable], 20861 20862 /** 20863 * Renders the control as a HTML string. 20864 * 20865 * @method renderHtml 20866 * @return {String} HTML representing the control. 20867 */ 20868 renderHtml: function() { 20869 var self = this, layout = self._layout, innerHtml = self.settings.html; 20870 20871 self.preRender(); 20872 layout.preRender(self); 20873 20874 if (typeof(innerHtml) == "undefined") { 20875 innerHtml = ( 20876 '<div id="' + self._id + '-body" class="' + self.classes('body') + '">' + 20877 layout.renderHtml(self) + 20878 '</div>' 20879 ); 20880 } else { 20881 if (typeof(innerHtml) == 'function') { 20882 innerHtml = innerHtml.call(self); 20883 } 20884 20885 self._hasBody = false; 20886 } 20887 20888 return ( 20889 '<div id="' + self._id + '" class="' + self.classes() + '" hidefocus="1" tabindex="-1" role="group">' + 20890 (self._preBodyHtml || '') + 20891 innerHtml + 20892 '</div>' 20893 ); 20894 } 20895 }); 20896 }); 20897 20898 // Included from: js/tinymce/classes/ui/Movable.js 20899 20900 /** 20901 * Movable.js 20902 * 20903 * Copyright, Moxiecode Systems AB 20904 * Released under LGPL License. 20905 * 20906 * License: http://www.tinymce.com/license 20907 * Contributing: http://www.tinymce.com/contributing 20908 */ 20909 20910 /** 20911 * Movable mixin. Makes controls movable absolute and relative to other elements. 20912 * 20913 * @mixin tinymce.ui.Movable 20914 */ 20915 define("tinymce/ui/Movable", [ 20916 "tinymce/ui/DomUtils" 20917 ], function(DomUtils) { 20918 "use strict"; 20919 20920 function calculateRelativePosition(ctrl, targetElm, rel) { 20921 var ctrlElm, pos, x, y, selfW, selfH, targetW, targetH, viewport, size; 20922 20923 viewport = DomUtils.getViewPort(); 20924 20925 // Get pos of target 20926 pos = DomUtils.getPos(targetElm); 20927 x = pos.x; 20928 y = pos.y; 20929 20930 if (ctrl._fixed) { 20931 x -= viewport.x; 20932 y -= viewport.y; 20933 } 20934 20935 // Get size of self 20936 ctrlElm = ctrl.getEl(); 20937 size = DomUtils.getSize(ctrlElm); 20938 selfW = size.width; 20939 selfH = size.height; 20940 20941 // Get size of target 20942 size = DomUtils.getSize(targetElm); 20943 targetW = size.width; 20944 targetH = size.height; 20945 20946 // Parse align string 20947 rel = (rel || '').split(''); 20948 20949 // Target corners 20950 if (rel[0] === 'b') { 20951 y += targetH; 20952 } 20953 20954 if (rel[1] === 'r') { 20955 x += targetW; 20956 } 20957 20958 if (rel[0] === 'c') { 20959 y += Math.round(targetH / 2); 20960 } 20961 20962 if (rel[1] === 'c') { 20963 x += Math.round(targetW / 2); 20964 } 20965 20966 // Self corners 20967 if (rel[3] === 'b') { 20968 y -= selfH; 20969 } 20970 20971 if (rel[4] === 'r') { 20972 x -= selfW; 20973 } 20974 20975 if (rel[3] === 'c') { 20976 y -= Math.round(selfH / 2); 20977 } 20978 20979 if (rel[4] === 'c') { 20980 x -= Math.round(selfW / 2); 20981 } 20982 20983 return { 20984 x: x, 20985 y: y, 20986 w: selfW, 20987 h: selfH 20988 }; 20989 } 20990 20991 return { 20992 /** 20993 * Tests various positions to get the most suitable one. 20994 * 20995 * @method testMoveRel 20996 * @param {DOMElement} elm Element to position against. 20997 * @param {Array} rels Array with relative positions. 20998 * @return {String} Best suitable relative position. 20999 */ 21000 testMoveRel: function(elm, rels) { 21001 var viewPortRect = DomUtils.getViewPort(); 21002 21003 for (var i = 0; i < rels.length; i++) { 21004 var pos = calculateRelativePosition(this, elm, rels[i]); 21005 21006 if (this._fixed) { 21007 if (pos.x > 0 && pos.x + pos.w < viewPortRect.w && pos.y > 0 && pos.y + pos.h < viewPortRect.h) { 21008 return rels[i]; 21009 } 21010 } else { 21011 if (pos.x > viewPortRect.x && pos.x + pos.w < viewPortRect.w + viewPortRect.x && 21012 pos.y > viewPortRect.y && pos.y + pos.h < viewPortRect.h + viewPortRect.y) { 21013 return rels[i]; 21014 } 21015 } 21016 } 21017 21018 return rels[0]; 21019 }, 21020 21021 /** 21022 * Move relative to the specified element. 21023 * 21024 * @method moveRel 21025 * @param {Element} elm Element to move relative to. 21026 * @param {String} rel Relative mode. For example: br-tl. 21027 * @return {tinymce.ui.Control} Current control instance. 21028 */ 21029 moveRel: function(elm, rel) { 21030 if (typeof(rel) != 'string') { 21031 rel = this.testMoveRel(elm, rel); 21032 } 21033 21034 var pos = calculateRelativePosition(this, elm, rel); 21035 return this.moveTo(pos.x, pos.y); 21036 }, 21037 21038 /** 21039 * Move by a relative x, y values. 21040 * 21041 * @method moveBy 21042 * @param {Number} dx Relative x position. 21043 * @param {Number} dy Relative y position. 21044 * @return {tinymce.ui.Control} Current control instance. 21045 */ 21046 moveBy: function(dx, dy) { 21047 var self = this, rect = self.layoutRect(); 21048 21049 self.moveTo(rect.x + dx, rect.y + dy); 21050 21051 return self; 21052 }, 21053 21054 /** 21055 * Move to absolute position. 21056 * 21057 * @method moveTo 21058 * @param {Number} x Absolute x position. 21059 * @param {Number} y Absolute y position. 21060 * @return {tinymce.ui.Control} Current control instance. 21061 */ 21062 moveTo: function(x, y) { 21063 var self = this; 21064 21065 // TODO: Move this to some global class 21066 function contrain(value, max, size) { 21067 if (value < 0) { 21068 return 0; 21069 } 21070 21071 if (value + size > max) { 21072 value = max - size; 21073 return value < 0 ? 0 : value; 21074 } 21075 21076 return value; 21077 } 21078 21079 if (self.settings.constrainToViewport) { 21080 var viewPortRect = DomUtils.getViewPort(window); 21081 var layoutRect = self.layoutRect(); 21082 21083 x = contrain(x, viewPortRect.w + viewPortRect.x, layoutRect.w); 21084 y = contrain(y, viewPortRect.h + viewPortRect.y, layoutRect.h); 21085 } 21086 21087 if (self._rendered) { 21088 self.layoutRect({x: x, y: y}).repaint(); 21089 } else { 21090 self.settings.x = x; 21091 self.settings.y = y; 21092 } 21093 21094 self.fire('move', {x: x, y: y}); 21095 21096 return self; 21097 } 21098 }; 21099 }); 21100 21101 // Included from: js/tinymce/classes/ui/Resizable.js 21102 21103 /** 21104 * Resizable.js 21105 * 21106 * Copyright, Moxiecode Systems AB 21107 * Released under LGPL License. 21108 * 21109 * License: http://www.tinymce.com/license 21110 * Contributing: http://www.tinymce.com/contributing 21111 */ 21112 21113 /** 21114 * Resizable mixin. Enables controls to be resized. 21115 * 21116 * @mixin tinymce.ui.Resizable 21117 */ 21118 define("tinymce/ui/Resizable", [ 21119 "tinymce/ui/DomUtils" 21120 ], function(DomUtils) { 21121 "use strict"; 21122 21123 return { 21124 /** 21125 * Resizes the control to contents. 21126 * 21127 * @method resizeToContent 21128 */ 21129 resizeToContent: function() { 21130 this._layoutRect.autoResize = true; 21131 this._lastRect = null; 21132 this.reflow(); 21133 }, 21134 21135 /** 21136 * Resizes the control to a specific width/height. 21137 * 21138 * @method resizeTo 21139 * @param {Number} w Control width. 21140 * @param {Number} h Control height. 21141 * @return {tinymce.ui.Control} Current control instance. 21142 */ 21143 resizeTo: function(w, h) { 21144 // TODO: Fix hack 21145 if (w <= 1 || h <= 1) { 21146 var rect = DomUtils.getWindowSize(); 21147 21148 w = w <= 1 ? w * rect.w : w; 21149 h = h <= 1 ? h * rect.h : h; 21150 } 21151 21152 this._layoutRect.autoResize = false; 21153 return this.layoutRect({minW: w, minH: h, w: w, h: h}).reflow(); 21154 }, 21155 21156 /** 21157 * Resizes the control to a specific relative width/height. 21158 * 21159 * @method resizeBy 21160 * @param {Number} dw Relative control width. 21161 * @param {Number} dh Relative control height. 21162 * @return {tinymce.ui.Control} Current control instance. 21163 */ 21164 resizeBy: function(dw, dh) { 21165 var self = this, rect = self.layoutRect(); 21166 21167 return self.resizeTo(rect.w + dw, rect.h + dh); 21168 } 21169 }; 21170 }); 21171 21172 // Included from: js/tinymce/classes/ui/FloatPanel.js 21173 21174 /** 21175 * FloatPanel.js 21176 * 21177 * Copyright, Moxiecode Systems AB 21178 * Released under LGPL License. 21179 * 21180 * License: http://www.tinymce.com/license 21181 * Contributing: http://www.tinymce.com/contributing 21182 */ 21183 21184 /** 21185 * This class creates a floating panel. 21186 * 21187 * @-x-less FloatPanel.less 21188 * @class tinymce.ui.FloatPanel 21189 * @extends tinymce.ui.Panel 21190 * @mixes tinymce.ui.Movable 21191 * @mixes tinymce.ui.Resizable 21192 */ 21193 define("tinymce/ui/FloatPanel", [ 21194 "tinymce/ui/Panel", 21195 "tinymce/ui/Movable", 21196 "tinymce/ui/Resizable", 21197 "tinymce/ui/DomUtils" 21198 ], function(Panel, Movable, Resizable, DomUtils) { 21199 "use strict"; 21200 21201 var documentClickHandler, documentScrollHandler, visiblePanels = []; 21202 var zOrder = [], hasModal; 21203 21204 var FloatPanel = Panel.extend({ 21205 Mixins: [Movable, Resizable], 21206 21207 /** 21208 * Constructs a new control instance with the specified settings. 21209 * 21210 * @constructor 21211 * @param {Object} settings Name/value object with settings. 21212 * @setting {Boolean} autohide Automatically hide the panel. 21213 */ 21214 init: function(settings) { 21215 var self = this; 21216 21217 function reorder() { 21218 var i, zIndex = FloatPanel.zIndex || 0xFFFF, topModal; 21219 21220 if (zOrder.length) { 21221 for (i = 0; i < zOrder.length; i++) { 21222 if (zOrder[i].modal) { 21223 zIndex++; 21224 topModal = zOrder[i]; 21225 } 21226 21227 zOrder[i].getEl().style.zIndex = zIndex; 21228 zOrder[i].zIndex = zIndex; 21229 zIndex++; 21230 } 21231 } 21232 21233 var modalBlockEl = document.getElementById(self.classPrefix + 'modal-block'); 21234 21235 if (topModal) { 21236 DomUtils.css(modalBlockEl, 'z-index', topModal.zIndex - 1); 21237 } else if (modalBlockEl) { 21238 modalBlockEl.parentNode.removeChild(modalBlockEl); 21239 hasModal = false; 21240 } 21241 21242 FloatPanel.currentZIndex = zIndex; 21243 } 21244 21245 function isChildOf(ctrl, parent) { 21246 while (ctrl) { 21247 if (ctrl == parent) { 21248 return true; 21249 } 21250 21251 ctrl = ctrl.parent(); 21252 } 21253 } 21254 21255 /** 21256 * Repositions the panel to the top of page if the panel is outside of the visual viewport. It will 21257 * also reposition all child panels of the current panel. 21258 */ 21259 function repositionPanel(panel) { 21260 var scrollY = DomUtils.getViewPort().y; 21261 21262 function toggleFixedChildPanels(fixed, deltaY) { 21263 var parent; 21264 21265 for (var i = 0; i < visiblePanels.length; i++) { 21266 if (visiblePanels[i] != panel) { 21267 parent = visiblePanels[i].parent(); 21268 21269 while (parent && (parent = parent.parent())) { 21270 if (parent == panel) { 21271 visiblePanels[i].fixed(fixed).moveBy(0, deltaY).repaint(); 21272 } 21273 } 21274 } 21275 } 21276 } 21277 21278 if (panel.settings.autofix) { 21279 if (!panel._fixed) { 21280 panel._autoFixY = panel.layoutRect().y; 21281 21282 if (panel._autoFixY < scrollY) { 21283 panel.fixed(true).layoutRect({y: 0}).repaint(); 21284 toggleFixedChildPanels(true, scrollY - panel._autoFixY); 21285 } 21286 } else { 21287 if (panel._autoFixY > scrollY) { 21288 panel.fixed(false).layoutRect({y: panel._autoFixY}).repaint(); 21289 toggleFixedChildPanels(false, panel._autoFixY - scrollY); 21290 } 21291 } 21292 } 21293 } 21294 21295 self._super(settings); 21296 self._eventsRoot = self; 21297 21298 self.addClass('floatpanel'); 21299 21300 // Hide floatpanes on click out side the root button 21301 if (settings.autohide) { 21302 if (!documentClickHandler) { 21303 documentClickHandler = function(e) { 21304 // Hide any float panel when a click is out side that float panel and the 21305 // float panels direct parent for example a click on a menu button 21306 var i = visiblePanels.length; 21307 while (i--) { 21308 var panel = visiblePanels[i], clickCtrl = panel.getParentCtrl(e.target); 21309 21310 if (panel.settings.autohide) { 21311 if (clickCtrl) { 21312 if (isChildOf(clickCtrl, panel) || panel.parent() === clickCtrl) { 21313 continue; 21314 } 21315 } 21316 21317 e = panel.fire('autohide', {target: e.target}); 21318 if (!e.isDefaultPrevented()) { 21319 panel.hide(); 21320 } 21321 } 21322 } 21323 }; 21324 21325 DomUtils.on(document, 'click', documentClickHandler); 21326 } 21327 21328 visiblePanels.push(self); 21329 } 21330 21331 if (settings.autofix) { 21332 if (!documentScrollHandler) { 21333 documentScrollHandler = function() { 21334 var i; 21335 21336 i = visiblePanels.length; 21337 while (i--) { 21338 repositionPanel(visiblePanels[i]); 21339 } 21340 }; 21341 21342 DomUtils.on(window, 'scroll', documentScrollHandler); 21343 } 21344 21345 self.on('move', function() { 21346 repositionPanel(this); 21347 }); 21348 } 21349 21350 self.on('postrender show', function(e) { 21351 if (e.control == self) { 21352 var modalBlockEl, prefix = self.classPrefix; 21353 21354 if (self.modal && !hasModal) { 21355 modalBlockEl = DomUtils.createFragment('<div id="' + prefix + 'modal-block" class="' + 21356 prefix + 'reset ' + prefix + 'fade"></div>'); 21357 modalBlockEl = modalBlockEl.firstChild; 21358 21359 self.getContainerElm().appendChild(modalBlockEl); 21360 21361 setTimeout(function() { 21362 DomUtils.addClass(modalBlockEl, prefix + 'in'); 21363 DomUtils.addClass(self.getEl(), prefix + 'in'); 21364 }, 0); 21365 21366 hasModal = true; 21367 } 21368 21369 zOrder.push(self); 21370 reorder(); 21371 } 21372 }); 21373 21374 self.on('close hide', function(e) { 21375 if (e.control == self) { 21376 var i = zOrder.length; 21377 21378 while (i--) { 21379 if (zOrder[i] === self) { 21380 zOrder.splice(i, 1); 21381 } 21382 } 21383 21384 reorder(); 21385 } 21386 }); 21387 21388 self.on('show', function() { 21389 self.parents().each(function(ctrl) { 21390 if (ctrl._fixed) { 21391 self.fixed(true); 21392 return false; 21393 } 21394 }); 21395 }); 21396 21397 if (settings.popover) { 21398 self._preBodyHtml = '<div class="' + self.classPrefix + 'arrow"></div>'; 21399 self.addClass('popover').addClass('bottom').addClass(self.isRtl() ? 'end' : 'start'); 21400 } 21401 }, 21402 21403 fixed: function(state) { 21404 var self = this; 21405 21406 if (self._fixed != state) { 21407 if (self._rendered) { 21408 var viewport = DomUtils.getViewPort(); 21409 21410 if (state) { 21411 self.layoutRect().y -= viewport.y; 21412 } else { 21413 self.layoutRect().y += viewport.y; 21414 } 21415 } 21416 21417 self.toggleClass('fixed', state); 21418 self._fixed = state; 21419 } 21420 21421 return self; 21422 }, 21423 21424 /** 21425 * Shows the current float panel. 21426 * 21427 * @method show 21428 * @return {tinymce.ui.FloatPanel} Current floatpanel instance. 21429 */ 21430 show: function() { 21431 var self = this, i, state = self._super(); 21432 21433 i = visiblePanels.length; 21434 while (i--) { 21435 if (visiblePanels[i] === self) { 21436 break; 21437 } 21438 } 21439 21440 if (i === -1) { 21441 visiblePanels.push(self); 21442 } 21443 21444 return state; 21445 }, 21446 21447 /** 21448 * Hides the current float panel. 21449 * 21450 * @method hide 21451 * @return {tinymce.ui.FloatPanel} Current floatpanel instance. 21452 */ 21453 hide: function() { 21454 removeVisiblePanel(this); 21455 return this._super(); 21456 }, 21457 21458 /** 21459 * Hides all visible the float panels. 21460 * 21461 * @method hideAll 21462 */ 21463 hideAll: function() { 21464 FloatPanel.hideAll(); 21465 }, 21466 21467 /** 21468 * Closes the float panel. This will remove the float panel from page and fire the close event. 21469 * 21470 * @method close 21471 */ 21472 close: function() { 21473 var self = this; 21474 21475 self.fire('close'); 21476 21477 return self.remove(); 21478 }, 21479 21480 /** 21481 * Removes the float panel from page. 21482 * 21483 * @method remove 21484 */ 21485 remove: function() { 21486 removeVisiblePanel(this); 21487 this._super(); 21488 }, 21489 21490 postRender: function() { 21491 var self = this; 21492 21493 if (self.settings.bodyRole) { 21494 this.getEl('body').setAttribute('role', self.settings.bodyRole); 21495 } 21496 21497 return self._super(); 21498 } 21499 }); 21500 21501 /** 21502 * Hides all visible the float panels. 21503 * 21504 * @static 21505 * @method hideAll 21506 */ 21507 FloatPanel.hideAll = function() { 21508 var i = visiblePanels.length; 21509 21510 while (i--) { 21511 var panel = visiblePanels[i]; 21512 21513 if (panel && panel.settings.autohide) { 21514 panel.hide(); 21515 visiblePanels.splice(i, 1); 21516 } 21517 } 21518 }; 21519 21520 function removeVisiblePanel(panel) { 21521 var i; 21522 21523 i = visiblePanels.length; 21524 while (i--) { 21525 if (visiblePanels[i] === panel) { 21526 visiblePanels.splice(i, 1); 21527 } 21528 } 21529 21530 i = zOrder.length; 21531 while (i--) { 21532 if (zOrder[i] === panel) { 21533 zOrder.splice(i, 1); 21534 } 21535 } 21536 } 21537 21538 return FloatPanel; 21539 }); 21540 21541 // Included from: js/tinymce/classes/ui/Window.js 21542 21543 /** 21544 * Window.js 21545 * 21546 * Copyright, Moxiecode Systems AB 21547 * Released under LGPL License. 21548 * 21549 * License: http://www.tinymce.com/license 21550 * Contributing: http://www.tinymce.com/contributing 21551 */ 21552 21553 /** 21554 * Creates a new window. 21555 * 21556 * @-x-less Window.less 21557 * @class tinymce.ui.Window 21558 * @extends tinymce.ui.FloatPanel 21559 */ 21560 define("tinymce/ui/Window", [ 21561 "tinymce/ui/FloatPanel", 21562 "tinymce/ui/Panel", 21563 "tinymce/ui/DomUtils", 21564 "tinymce/ui/DragHelper" 21565 ], function(FloatPanel, Panel, DomUtils, DragHelper) { 21566 "use strict"; 21567 21568 var Window = FloatPanel.extend({ 21569 modal: true, 21570 21571 Defaults: { 21572 border: 1, 21573 layout: 'flex', 21574 containerCls: 'panel', 21575 role: 'dialog', 21576 callbacks: { 21577 submit: function() { 21578 this.fire('submit', {data: this.toJSON()}); 21579 }, 21580 21581 close: function() { 21582 this.close(); 21583 } 21584 } 21585 }, 21586 21587 /** 21588 * Constructs a instance with the specified settings. 21589 * 21590 * @constructor 21591 * @param {Object} settings Name/value object with settings. 21592 */ 21593 init: function(settings) { 21594 var self = this; 21595 21596 self._super(settings); 21597 21598 if (self.isRtl()) { 21599 self.addClass('rtl'); 21600 } 21601 21602 self.addClass('window'); 21603 self._fixed = true; 21604 21605 // Create statusbar 21606 if (settings.buttons) { 21607 self.statusbar = new Panel({ 21608 layout: 'flex', 21609 border: '1 0 0 0', 21610 spacing: 3, 21611 padding: 10, 21612 align: 'center', 21613 pack: self.isRtl() ? 'start' : 'end', 21614 defaults: { 21615 type: 'button' 21616 }, 21617 items: settings.buttons 21618 }); 21619 21620 self.statusbar.addClass('foot'); 21621 self.statusbar.parent(self); 21622 } 21623 21624 self.on('click', function(e) { 21625 if (e.target.className.indexOf(self.classPrefix + 'close') != -1) { 21626 self.close(); 21627 } 21628 }); 21629 21630 self.on('cancel', function() { 21631 self.close(); 21632 }); 21633 21634 self.aria('describedby', self.describedBy || self._id + '-none'); 21635 self.aria('label', settings.title); 21636 self._fullscreen = false; 21637 }, 21638 21639 /** 21640 * Recalculates the positions of the controls in the current container. 21641 * This is invoked by the reflow method and shouldn't be called directly. 21642 * 21643 * @method recalc 21644 */ 21645 recalc: function() { 21646 var self = this, statusbar = self.statusbar, layoutRect, width, x, needsRecalc; 21647 21648 if (self._fullscreen) { 21649 self.layoutRect(DomUtils.getWindowSize()); 21650 self.layoutRect().contentH = self.layoutRect().innerH; 21651 } 21652 21653 self._super(); 21654 21655 layoutRect = self.layoutRect(); 21656 21657 // Resize window based on title width 21658 if (self.settings.title && !self._fullscreen) { 21659 width = layoutRect.headerW; 21660 if (width > layoutRect.w) { 21661 x = layoutRect.x - Math.max(0, width / 2); 21662 self.layoutRect({w: width, x: x}); 21663 needsRecalc = true; 21664 } 21665 } 21666 21667 // Resize window based on statusbar width 21668 if (statusbar) { 21669 statusbar.layoutRect({w: self.layoutRect().innerW}).recalc(); 21670 21671 width = statusbar.layoutRect().minW + layoutRect.deltaW; 21672 if (width > layoutRect.w) { 21673 x = layoutRect.x - Math.max(0, width - layoutRect.w); 21674 self.layoutRect({w: width, x: x}); 21675 needsRecalc = true; 21676 } 21677 } 21678 21679 // Recalc body and disable auto resize 21680 if (needsRecalc) { 21681 self.recalc(); 21682 } 21683 }, 21684 21685 /** 21686 * Initializes the current controls layout rect. 21687 * This will be executed by the layout managers to determine the 21688 * default minWidth/minHeight etc. 21689 * 21690 * @method initLayoutRect 21691 * @return {Object} Layout rect instance. 21692 */ 21693 initLayoutRect: function() { 21694 var self = this, layoutRect = self._super(), deltaH = 0, headEl; 21695 21696 // Reserve vertical space for title 21697 if (self.settings.title && !self._fullscreen) { 21698 headEl = self.getEl('head'); 21699 21700 var size = DomUtils.getSize(headEl); 21701 21702 layoutRect.headerW = size.width; 21703 layoutRect.headerH = size.height; 21704 21705 deltaH += layoutRect.headerH; 21706 } 21707 21708 // Reserve vertical space for statusbar 21709 if (self.statusbar) { 21710 deltaH += self.statusbar.layoutRect().h; 21711 } 21712 21713 layoutRect.deltaH += deltaH; 21714 layoutRect.minH += deltaH; 21715 //layoutRect.innerH -= deltaH; 21716 layoutRect.h += deltaH; 21717 21718 var rect = DomUtils.getWindowSize(); 21719 21720 layoutRect.x = Math.max(0, rect.w / 2 - layoutRect.w / 2); 21721 layoutRect.y = Math.max(0, rect.h / 2 - layoutRect.h / 2); 21722 21723 return layoutRect; 21724 }, 21725 21726 /** 21727 * Renders the control as a HTML string. 21728 * 21729 * @method renderHtml 21730 * @return {String} HTML representing the control. 21731 */ 21732 renderHtml: function() { 21733 var self = this, layout = self._layout, id = self._id, prefix = self.classPrefix; 21734 var settings = self.settings, headerHtml = '', footerHtml = '', html = settings.html; 21735 21736 self.preRender(); 21737 layout.preRender(self); 21738 21739 if (settings.title) { 21740 headerHtml = ( 21741 '<div id="' + id + '-head" class="' + prefix + 'window-head">' + 21742 '<div id="' + id + '-title" class="' + prefix + 'title">' + self.encode(settings.title) + '</div>' + 21743 '<button type="button" class="' + prefix + 'close" aria-hidden="true">\u00d7</button>' + 21744 '<div id="' + id + '-dragh" class="' + prefix + 'dragh"></div>' + 21745 '</div>' 21746 ); 21747 } 21748 21749 if (settings.url) { 21750 html = '<iframe src="' + settings.url + '" tabindex="-1"></iframe>'; 21751 } 21752 21753 if (typeof(html) == "undefined") { 21754 html = layout.renderHtml(self); 21755 } 21756 21757 if (self.statusbar) { 21758 footerHtml = self.statusbar.renderHtml(); 21759 } 21760 21761 return ( 21762 '<div id="' + id + '" class="' + self.classes() + '" hidefocus="1">' + 21763 '<div class="' + self.classPrefix + 'reset" role="application">' + 21764 headerHtml + 21765 '<div id="' + id + '-body" class="' + self.classes('body') + '">' + 21766 html + 21767 '</div>' + 21768 footerHtml + 21769 '</div>' + 21770 '</div>' 21771 ); 21772 }, 21773 21774 /** 21775 * Switches the window fullscreen mode. 21776 * 21777 * @method fullscreen 21778 * @param {Boolean} state True/false state. 21779 * @return {tinymce.ui.Window} Current window instance. 21780 */ 21781 fullscreen: function(state) { 21782 var self = this, documentElement = document.documentElement, slowRendering, prefix = self.classPrefix, layoutRect; 21783 21784 if (state != self._fullscreen) { 21785 DomUtils.on(window, 'resize', function() { 21786 var time; 21787 21788 if (self._fullscreen) { 21789 // Time the layout time if it's to slow use a timeout to not hog the CPU 21790 if (!slowRendering) { 21791 time = new Date().getTime(); 21792 21793 var rect = DomUtils.getWindowSize(); 21794 self.moveTo(0, 0).resizeTo(rect.w, rect.h); 21795 21796 if ((new Date().getTime()) - time > 50) { 21797 slowRendering = true; 21798 } 21799 } else { 21800 if (!self._timer) { 21801 self._timer = setTimeout(function() { 21802 var rect = DomUtils.getWindowSize(); 21803 self.moveTo(0, 0).resizeTo(rect.w, rect.h); 21804 21805 self._timer = 0; 21806 }, 50); 21807 } 21808 } 21809 } 21810 }); 21811 21812 layoutRect = self.layoutRect(); 21813 self._fullscreen = state; 21814 21815 if (!state) { 21816 self._borderBox = self.parseBox(self.settings.border); 21817 self.getEl('head').style.display = ''; 21818 layoutRect.deltaH += layoutRect.headerH; 21819 DomUtils.removeClass(documentElement, prefix + 'fullscreen'); 21820 DomUtils.removeClass(document.body, prefix + 'fullscreen'); 21821 self.removeClass('fullscreen'); 21822 self.moveTo(self._initial.x, self._initial.y).resizeTo(self._initial.w, self._initial.h); 21823 } else { 21824 self._initial = {x: layoutRect.x, y: layoutRect.y, w: layoutRect.w, h: layoutRect.h}; 21825 21826 self._borderBox = self.parseBox('0'); 21827 self.getEl('head').style.display = 'none'; 21828 layoutRect.deltaH -= layoutRect.headerH + 2; 21829 DomUtils.addClass(documentElement, prefix + 'fullscreen'); 21830 DomUtils.addClass(document.body, prefix + 'fullscreen'); 21831 self.addClass('fullscreen'); 21832 21833 var rect = DomUtils.getWindowSize(); 21834 self.moveTo(0, 0).resizeTo(rect.w, rect.h); 21835 } 21836 } 21837 21838 return self.reflow(); 21839 }, 21840 21841 /** 21842 * Called after the control has been rendered. 21843 * 21844 * @method postRender 21845 */ 21846 postRender: function() { 21847 var self = this, startPos; 21848 21849 setTimeout(function() { 21850 self.addClass('in'); 21851 }, 0); 21852 21853 self._super(); 21854 21855 if (self.statusbar) { 21856 self.statusbar.postRender(); 21857 } 21858 21859 self.focus(); 21860 21861 this.dragHelper = new DragHelper(self._id + '-dragh', { 21862 start: function() { 21863 startPos = { 21864 x: self.layoutRect().x, 21865 y: self.layoutRect().y 21866 }; 21867 }, 21868 21869 drag: function(e) { 21870 self.moveTo(startPos.x + e.deltaX, startPos.y + e.deltaY); 21871 } 21872 }); 21873 21874 self.on('submit', function(e) { 21875 if (!e.isDefaultPrevented()) { 21876 self.close(); 21877 } 21878 }); 21879 }, 21880 21881 /** 21882 * Fires a submit event with the serialized form. 21883 * 21884 * @method submit 21885 * @return {Object} Event arguments object. 21886 */ 21887 submit: function() { 21888 return this.fire('submit', {data: this.toJSON()}); 21889 }, 21890 21891 /** 21892 * Removes the current control from DOM and from UI collections. 21893 * 21894 * @method remove 21895 * @return {tinymce.ui.Control} Current control instance. 21896 */ 21897 remove: function() { 21898 var self = this, prefix = self.classPrefix; 21899 21900 self.dragHelper.destroy(); 21901 self._super(); 21902 21903 if (self.statusbar) { 21904 this.statusbar.remove(); 21905 } 21906 21907 if (self._fullscreen) { 21908 DomUtils.removeClass(document.documentElement, prefix + 'fullscreen'); 21909 DomUtils.removeClass(document.body, prefix + 'fullscreen'); 21910 } 21911 }, 21912 21913 /** 21914 * Returns the contentWindow object of the iframe if it exists. 21915 * 21916 * @method getContentWindow 21917 * @return {Window} window object or null. 21918 */ 21919 getContentWindow: function() { 21920 var ifr = this.getEl().getElementsByTagName('iframe')[0]; 21921 return ifr ? ifr.contentWindow : null; 21922 } 21923 }); 21924 21925 return Window; 21926 }); 21927 21928 // Included from: js/tinymce/classes/ui/MessageBox.js 21929 21930 /** 21931 * MessageBox.js 21932 * 21933 * Copyright, Moxiecode Systems AB 21934 * Released under LGPL License. 21935 * 21936 * License: http://www.tinymce.com/license 21937 * Contributing: http://www.tinymce.com/contributing 21938 */ 21939 21940 /** 21941 * This class is used to create MessageBoxes like alerts/confirms etc. 21942 * 21943 * @class tinymce.ui.Window 21944 * @extends tinymce.ui.FloatPanel 21945 */ 21946 define("tinymce/ui/MessageBox", [ 21947 "tinymce/ui/Window" 21948 ], function(Window) { 21949 "use strict"; 21950 21951 var MessageBox = Window.extend({ 21952 /** 21953 * Constructs a instance with the specified settings. 21954 * 21955 * @constructor 21956 * @param {Object} settings Name/value object with settings. 21957 */ 21958 init: function(settings) { 21959 settings = { 21960 border: 1, 21961 padding: 20, 21962 layout: 'flex', 21963 pack: "center", 21964 align: "center", 21965 containerCls: 'panel', 21966 autoScroll: true, 21967 buttons: {type: "button", text: "Ok", action: "ok"}, 21968 items: { 21969 type: "label", 21970 multiline: true, 21971 maxWidth: 500, 21972 maxHeight: 200 21973 } 21974 }; 21975 21976 this._super(settings); 21977 }, 21978 21979 Statics: { 21980 /** 21981 * Ok buttons constant. 21982 * 21983 * @static 21984 * @final 21985 * @field {Number} OK 21986 */ 21987 OK: 1, 21988 21989 /** 21990 * Ok/cancel buttons constant. 21991 * 21992 * @static 21993 * @final 21994 * @field {Number} OK_CANCEL 21995 */ 21996 OK_CANCEL: 2, 21997 21998 /** 21999 * yes/no buttons constant. 22000 * 22001 * @static 22002 * @final 22003 * @field {Number} YES_NO 22004 */ 22005 YES_NO: 3, 22006 22007 /** 22008 * yes/no/cancel buttons constant. 22009 * 22010 * @static 22011 * @final 22012 * @field {Number} YES_NO_CANCEL 22013 */ 22014 YES_NO_CANCEL: 4, 22015 22016 /** 22017 * Constructs a new message box and renders it to the body element. 22018 * 22019 * @static 22020 * @method msgBox 22021 * @param {Object} settings Name/value object with settings. 22022 */ 22023 msgBox: function(settings) { 22024 var buttons, callback = settings.callback || function() {}; 22025 22026 switch (settings.buttons) { 22027 case MessageBox.OK_CANCEL: 22028 buttons = [ 22029 {type: "button", text: "Ok", subtype: "primary", onClick: function(e) { 22030 e.control.parents()[1].close(); 22031 callback(true); 22032 }}, 22033 22034 {type: "button", text: "Cancel", onClick: function(e) { 22035 e.control.parents()[1].close(); 22036 callback(false); 22037 }} 22038 ]; 22039 break; 22040 22041 case MessageBox.YES_NO: 22042 buttons = [ 22043 {type: "button", text: "Ok", subtype: "primary", onClick: function(e) { 22044 e.control.parents()[1].close(); 22045 callback(true); 22046 }} 22047 ]; 22048 break; 22049 22050 case MessageBox.YES_NO_CANCEL: 22051 buttons = [ 22052 {type: "button", text: "Ok", subtype: "primary", onClick: function(e) { 22053 e.control.parents()[1].close(); 22054 }} 22055 ]; 22056 break; 22057 22058 default: 22059 buttons = [ 22060 {type: "button", text: "Ok", subtype: "primary", onClick: function(e) { 22061 e.control.parents()[1].close(); 22062 callback(true); 22063 }} 22064 ]; 22065 break; 22066 } 22067 22068 return new Window({ 22069 padding: 20, 22070 x: settings.x, 22071 y: settings.y, 22072 minWidth: 300, 22073 minHeight: 100, 22074 layout: "flex", 22075 pack: "center", 22076 align: "center", 22077 buttons: buttons, 22078 title: settings.title, 22079 role: 'alertdialog', 22080 items: { 22081 type: "label", 22082 multiline: true, 22083 maxWidth: 500, 22084 maxHeight: 200, 22085 text: settings.text 22086 }, 22087 onPostRender: function() { 22088 this.aria('describedby', this.items()[0]._id); 22089 }, 22090 onClose: settings.onClose, 22091 onCancel: function() { 22092 callback(false); 22093 } 22094 }).renderTo(document.body).reflow(); 22095 }, 22096 22097 /** 22098 * Creates a new alert dialog. 22099 * 22100 * @method alert 22101 * @param {Object} settings Settings for the alert dialog. 22102 * @param {function} [callback] Callback to execute when the user makes a choice. 22103 */ 22104 alert: function(settings, callback) { 22105 if (typeof(settings) == "string") { 22106 settings = {text: settings}; 22107 } 22108 22109 settings.callback = callback; 22110 return MessageBox.msgBox(settings); 22111 }, 22112 22113 /** 22114 * Creates a new confirm dialog. 22115 * 22116 * @method confirm 22117 * @param {Object} settings Settings for the confirm dialog. 22118 * @param {function} [callback] Callback to execute when the user makes a choice. 22119 */ 22120 confirm: function(settings, callback) { 22121 if (typeof(settings) == "string") { 22122 settings = {text: settings}; 22123 } 22124 22125 settings.callback = callback; 22126 settings.buttons = MessageBox.OK_CANCEL; 22127 22128 return MessageBox.msgBox(settings); 22129 } 22130 } 22131 }); 22132 22133 return MessageBox; 22134 }); 22135 22136 // Included from: js/tinymce/classes/WindowManager.js 22137 22138 /** 22139 * WindowManager.js 22140 * 22141 * Copyright, Moxiecode Systems AB 22142 * Released under LGPL License. 22143 * 22144 * License: http://www.tinymce.com/license 22145 * Contributing: http://www.tinymce.com/contributing 22146 */ 22147 22148 /** 22149 * This class handles the creation of native windows and dialogs. This class can be extended to provide for example inline dialogs. 22150 * 22151 * @class tinymce.WindowManager 22152 * @example 22153 * // Opens a new dialog with the file.htm file and the size 320x240 22154 * // It also adds a custom parameter this can be retrieved by using tinyMCEPopup.getWindowArg inside the dialog. 22155 * tinymce.activeEditor.windowManager.open({ 22156 * url: 'file.htm', 22157 * width: 320, 22158 * height: 240 22159 * }, { 22160 * custom_param: 1 22161 * }); 22162 * 22163 * // Displays an alert box using the active editors window manager instance 22164 * tinymce.activeEditor.windowManager.alert('Hello world!'); 22165 * 22166 * // Displays an confirm box and an alert message will be displayed depending on what you choose in the confirm 22167 * tinymce.activeEditor.windowManager.confirm("Do you want to do something", function(s) { 22168 * if (s) 22169 * tinymce.activeEditor.windowManager.alert("Ok"); 22170 * else 22171 * tinymce.activeEditor.windowManager.alert("Cancel"); 22172 * }); 22173 */ 22174 define("tinymce/WindowManager", [ 22175 "tinymce/ui/Window", 22176 "tinymce/ui/MessageBox" 22177 ], function(Window, MessageBox) { 22178 return function(editor) { 22179 var self = this, windows = []; 22180 22181 function getTopMostWindow() { 22182 if (windows.length) { 22183 return windows[windows.length - 1]; 22184 } 22185 } 22186 22187 self.windows = windows; 22188 22189 /** 22190 * Opens a new window. 22191 * 22192 * @method open 22193 * @param {Object} args Optional name/value settings collection contains things like width/height/url etc. 22194 * @option {String} title Window title. 22195 * @option {String} file URL of the file to open in the window. 22196 * @option {Number} width Width in pixels. 22197 * @option {Number} height Height in pixels. 22198 * @option {Boolean} resizable Specifies whether the popup window is resizable or not. 22199 * @option {Boolean} maximizable Specifies whether the popup window has a "maximize" button and can get maximized or not. 22200 * @option {String/Boolean} scrollbars Specifies whether the popup window can have scrollbars if required (i.e. content 22201 * larger than the popup size specified). 22202 */ 22203 self.open = function(args, params) { 22204 var win; 22205 22206 editor.editorManager.activeEditor = editor; 22207 22208 args.title = args.title || ' '; 22209 22210 // Handle URL 22211 args.url = args.url || args.file; // Legacy 22212 if (args.url) { 22213 args.width = parseInt(args.width || 320, 10); 22214 args.height = parseInt(args.height || 240, 10); 22215 } 22216 22217 // Handle body 22218 if (args.body) { 22219 args.items = { 22220 defaults: args.defaults, 22221 type: args.bodyType || 'form', 22222 items: args.body 22223 }; 22224 } 22225 22226 if (!args.url && !args.buttons) { 22227 args.buttons = [ 22228 {text: 'Ok', subtype: 'primary', onclick: function() { 22229 win.find('form')[0].submit(); 22230 }}, 22231 22232 {text: 'Cancel', onclick: function() { 22233 win.close(); 22234 }} 22235 ]; 22236 } 22237 22238 win = new Window(args); 22239 windows.push(win); 22240 22241 win.on('close', function() { 22242 var i = windows.length; 22243 22244 while (i--) { 22245 if (windows[i] === win) { 22246 windows.splice(i, 1); 22247 } 22248 } 22249 22250 editor.focus(); 22251 }); 22252 22253 // Handle data 22254 if (args.data) { 22255 win.on('postRender', function() { 22256 this.find('*').each(function(ctrl) { 22257 var name = ctrl.name(); 22258 22259 if (name in args.data) { 22260 ctrl.value(args.data[name]); 22261 } 22262 }); 22263 }); 22264 } 22265 22266 // store args and parameters 22267 win.features = args || {}; 22268 win.params = params || {}; 22269 22270 // Takes a snapshot in the FocusManager of the selection before focus is lost to dialog 22271 editor.nodeChanged(); 22272 22273 return win.renderTo().reflow(); 22274 }; 22275 22276 /** 22277 * Creates a alert dialog. Please don't use the blocking behavior of this 22278 * native version use the callback method instead then it can be extended. 22279 * 22280 * @method alert 22281 * @param {String} message Text to display in the new alert dialog. 22282 * @param {function} callback Callback function to be executed after the user has selected ok. 22283 * @param {Object} scope Optional scope to execute the callback in. 22284 * @example 22285 * // Displays an alert box using the active editors window manager instance 22286 * tinymce.activeEditor.windowManager.alert('Hello world!'); 22287 */ 22288 self.alert = function(message, callback, scope) { 22289 MessageBox.alert(message, function() { 22290 if (callback) { 22291 callback.call(scope || this); 22292 } else { 22293 editor.focus(); 22294 } 22295 }); 22296 }; 22297 22298 /** 22299 * Creates a confirm dialog. Please don't use the blocking behavior of this 22300 * native version use the callback method instead then it can be extended. 22301 * 22302 * @method confirm 22303 * @param {String} messageText to display in the new confirm dialog. 22304 * @param {function} callback Callback function to be executed after the user has selected ok or cancel. 22305 * @param {Object} scope Optional scope to execute the callback in. 22306 * @example 22307 * // Displays an confirm box and an alert message will be displayed depending on what you choose in the confirm 22308 * tinymce.activeEditor.windowManager.confirm("Do you want to do something", function(s) { 22309 * if (s) 22310 * tinymce.activeEditor.windowManager.alert("Ok"); 22311 * else 22312 * tinymce.activeEditor.windowManager.alert("Cancel"); 22313 * }); 22314 */ 22315 self.confirm = function(message, callback, scope) { 22316 MessageBox.confirm(message, function(state) { 22317 callback.call(scope || this, state); 22318 }); 22319 }; 22320 22321 /** 22322 * Closes the top most window. 22323 * 22324 * @method close 22325 */ 22326 self.close = function() { 22327 if (getTopMostWindow()) { 22328 getTopMostWindow().close(); 22329 } 22330 }; 22331 22332 /** 22333 * Returns the params of the last window open call. This can be used in iframe based 22334 * dialog to get params passed from the tinymce plugin. 22335 * 22336 * @example 22337 * var dialogArguments = top.tinymce.activeEditor.windowManager.getParams(); 22338 * 22339 * @method getParams 22340 * @return {Object} Name/value object with parameters passed from windowManager.open call. 22341 */ 22342 self.getParams = function() { 22343 return getTopMostWindow() ? getTopMostWindow().params : null; 22344 }; 22345 22346 /** 22347 * Sets the params of the last opened window. 22348 * 22349 * @method setParams 22350 * @param {Object} params Params object to set for the last opened window. 22351 */ 22352 self.setParams = function(params) { 22353 if (getTopMostWindow()) { 22354 getTopMostWindow().params = params; 22355 } 22356 }; 22357 22358 /** 22359 * Returns the currently opened window objects. 22360 * 22361 * @method getWindows 22362 * @return {Array} Array of the currently opened windows. 22363 */ 22364 self.getWindows = function() { 22365 return windows; 22366 }; 22367 }; 22368 }); 22369 22370 // Included from: js/tinymce/classes/util/Quirks.js 22371 22372 /** 22373 * Quirks.js 22374 * 22375 * Copyright, Moxiecode Systems AB 22376 * Released under LGPL License. 22377 * 22378 * License: http://www.tinymce.com/license 22379 * Contributing: http://www.tinymce.com/contributing 22380 * 22381 * @ignore-file 22382 */ 22383 22384 /** 22385 * This file includes fixes for various browser quirks it's made to make it easy to add/remove browser specific fixes. 22386 * 22387 * @class tinymce.util.Quirks 22388 */ 22389 define("tinymce/util/Quirks", [ 22390 "tinymce/util/VK", 22391 "tinymce/dom/RangeUtils", 22392 "tinymce/html/Node", 22393 "tinymce/html/Entities", 22394 "tinymce/Env", 22395 "tinymce/util/Tools" 22396 ], function(VK, RangeUtils, Node, Entities, Env, Tools) { 22397 return function(editor) { 22398 var each = Tools.each; 22399 var BACKSPACE = VK.BACKSPACE, DELETE = VK.DELETE, dom = editor.dom, selection = editor.selection, 22400 settings = editor.settings, parser = editor.parser, serializer = editor.serializer; 22401 var isGecko = Env.gecko, isIE = Env.ie, isWebKit = Env.webkit; 22402 22403 /** 22404 * Executes a command with a specific state this can be to enable/disable browser editing features. 22405 */ 22406 function setEditorCommandState(cmd, state) { 22407 try { 22408 editor.getDoc().execCommand(cmd, false, state); 22409 } catch (ex) { 22410 // Ignore 22411 } 22412 } 22413 22414 /** 22415 * Returns current IE document mode. 22416 */ 22417 function getDocumentMode() { 22418 var documentMode = editor.getDoc().documentMode; 22419 22420 return documentMode ? documentMode : 6; 22421 } 22422 22423 /** 22424 * Returns true/false if the event is prevented or not. 22425 * 22426 * @private 22427 * @param {Event} e Event object. 22428 * @return {Boolean} true/false if the event is prevented or not. 22429 */ 22430 function isDefaultPrevented(e) { 22431 return e.isDefaultPrevented(); 22432 } 22433 22434 /** 22435 * Fixes a WebKit bug when deleting contents using backspace or delete key. 22436 * WebKit will produce a span element if you delete across two block elements. 22437 * 22438 * Example: 22439 * <h1>a</h1><p>|b</p> 22440 * 22441 * Will produce this on backspace: 22442 * <h1>a<span style="<all runtime styles>">b</span></p> 22443 * 22444 * This fixes the backspace to produce: 22445 * <h1>a|b</p> 22446 * 22447 * See bug: https://bugs.webkit.org/show_bug.cgi?id=45784 22448 * 22449 * This fixes the following delete scenarios: 22450 * 1. Delete by pressing backspace key. 22451 * 2. Delete by pressing delete key. 22452 * 3. Delete by pressing backspace key with ctrl/cmd (Word delete). 22453 * 4. Delete by pressing delete key with ctrl/cmd (Word delete). 22454 * 5. Delete by drag/dropping contents inside the editor. 22455 * 6. Delete by using Cut Ctrl+X/Cmd+X. 22456 * 7. Delete by selecting contents and writing a character.' 22457 * 22458 * This code is a ugly hack since writing full custom delete logic for just this bug 22459 * fix seemed like a huge task. I hope we can remove this before the year 2030. 22460 */ 22461 function cleanupStylesWhenDeleting() { 22462 var doc = editor.getDoc(), urlPrefix = 'data:text/mce-internal,'; 22463 var MutationObserver = window.MutationObserver, olderWebKit, dragStartRng; 22464 22465 // Add mini polyfill for older WebKits 22466 // TODO: Remove this when old Safari versions gets updated 22467 if (!MutationObserver) { 22468 olderWebKit = true; 22469 22470 MutationObserver = function() { 22471 var records = [], target; 22472 22473 function nodeInsert(e) { 22474 var target = e.relatedNode || e.target; 22475 records.push({target: target, addedNodes: [target]}); 22476 } 22477 22478 function attrModified(e) { 22479 var target = e.relatedNode || e.target; 22480 records.push({target: target, attributeName: e.attrName}); 22481 } 22482 22483 this.observe = function(node) { 22484 target = node; 22485 target.addEventListener('DOMSubtreeModified', nodeInsert, false); 22486 target.addEventListener('DOMNodeInsertedIntoDocument', nodeInsert, false); 22487 target.addEventListener('DOMNodeInserted', nodeInsert, false); 22488 target.addEventListener('DOMAttrModified', attrModified, false); 22489 }; 22490 22491 this.disconnect = function() { 22492 target.removeEventListener('DOMSubtreeModified', nodeInsert, false); 22493 target.removeEventListener('DOMNodeInsertedIntoDocument', nodeInsert, false); 22494 target.removeEventListener('DOMNodeInserted', nodeInsert, false); 22495 target.removeEventListener('DOMAttrModified', attrModified, false); 22496 }; 22497 22498 this.takeRecords = function() { 22499 return records; 22500 }; 22501 }; 22502 } 22503 22504 function customDelete(isForward) { 22505 var mutationObserver = new MutationObserver(function() {}); 22506 22507 Tools.each(editor.getBody().getElementsByTagName('*'), function(elm) { 22508 // Mark existing spans 22509 if (elm.tagName == 'SPAN') { 22510 elm.setAttribute('mce-data-marked', 1); 22511 } 22512 22513 // Make sure all elements has a data-mce-style attribute 22514 if (!elm.hasAttribute('data-mce-style') && elm.hasAttribute('style')) { 22515 editor.dom.setAttrib(elm, 'style', elm.getAttribute('style')); 22516 } 22517 }); 22518 22519 // Observe added nodes and style attribute changes 22520 mutationObserver.observe(editor.getDoc(), { 22521 childList: true, 22522 attributes: true, 22523 subtree: true, 22524 attributeFilter: ['style'] 22525 }); 22526 22527 editor.getDoc().execCommand(isForward ? 'ForwardDelete' : 'Delete', false, null); 22528 22529 var rng = editor.selection.getRng(); 22530 var caretElement = rng.startContainer.parentNode; 22531 22532 Tools.each(mutationObserver.takeRecords(), function(record) { 22533 if (!dom.isChildOf(record.target, editor.getBody())) { 22534 return; 22535 } 22536 22537 // Restore style attribute to previous value 22538 if (record.attributeName == "style") { 22539 var oldValue = record.target.getAttribute('data-mce-style'); 22540 22541 if (oldValue) { 22542 record.target.setAttribute("style", oldValue); 22543 } else { 22544 record.target.removeAttribute("style"); 22545 } 22546 } 22547 22548 // Remove all spans that isn't maked and retain selection 22549 Tools.each(record.addedNodes, function(node) { 22550 if (node.nodeName == "SPAN" && !node.getAttribute('mce-data-marked')) { 22551 var offset, container; 22552 22553 if (node == caretElement) { 22554 offset = rng.startOffset; 22555 container = node.firstChild; 22556 } 22557 22558 dom.remove(node, true); 22559 22560 if (container) { 22561 rng.setStart(container, offset); 22562 rng.setEnd(container, offset); 22563 editor.selection.setRng(rng); 22564 } 22565 } 22566 }); 22567 }); 22568 22569 mutationObserver.disconnect(); 22570 22571 // Remove any left over marks 22572 Tools.each(editor.dom.select('span[mce-data-marked]'), function(span) { 22573 span.removeAttribute('mce-data-marked'); 22574 }); 22575 } 22576 22577 editor.on('keydown', function(e) { 22578 var isForward = e.keyCode == DELETE, isMeta = VK.metaKeyPressed(e); 22579 22580 if (!isDefaultPrevented(e) && (isForward || e.keyCode == BACKSPACE)) { 22581 var rng = editor.selection.getRng(), container = rng.startContainer, offset = rng.startOffset; 22582 22583 // Ignore non meta delete in the where there is text before/after the caret 22584 if (!isMeta && rng.collapsed && container.nodeType == 3) { 22585 if (isForward ? offset < container.data.length : offset > 0) { 22586 return; 22587 } 22588 } 22589 22590 e.preventDefault(); 22591 22592 if (isMeta) { 22593 editor.selection.getSel().modify("extend", isForward ? "forward" : "backward", "word"); 22594 } 22595 22596 customDelete(isForward); 22597 } 22598 }); 22599 22600 editor.on('keypress', function(e) { 22601 if (!isDefaultPrevented(e) && !selection.isCollapsed() && e.charCode && !VK.metaKeyPressed(e)) { 22602 e.preventDefault(); 22603 customDelete(true); 22604 editor.selection.setContent(String.fromCharCode(e.charCode)); 22605 } 22606 }); 22607 22608 editor.addCommand('Delete', function() { 22609 customDelete(); 22610 }); 22611 22612 editor.addCommand('ForwardDelete', function() { 22613 customDelete(true); 22614 }); 22615 22616 // Older WebKits doesn't properly handle the clipboard so we can't add the rest 22617 if (olderWebKit) { 22618 return; 22619 } 22620 22621 editor.on('dragstart', function(e) { 22622 var selectionHtml; 22623 22624 if (editor.selection.isCollapsed() && e.target.tagName == 'IMG') { 22625 selection.select(e.target); 22626 } 22627 22628 dragStartRng = selection.getRng(); 22629 selectionHtml = editor.selection.getContent(); 22630 22631 // Safari doesn't support custom dataTransfer items so we can only use URL and Text 22632 if (selectionHtml.length > 0) { 22633 e.dataTransfer.setData('URL', 'data:text/mce-internal,' + escape(selectionHtml)); 22634 } 22635 }); 22636 22637 editor.on('drop', function(e) { 22638 if (!isDefaultPrevented(e)) { 22639 var internalContent = e.dataTransfer.getData('URL'); 22640 22641 if (!internalContent || internalContent.indexOf(urlPrefix) == -1 || !doc.caretRangeFromPoint) { 22642 return; 22643 } 22644 22645 internalContent = unescape(internalContent.substr(urlPrefix.length)); 22646 if (doc.caretRangeFromPoint) { 22647 e.preventDefault(); 22648 22649 // Safari has a weird issue where drag/dropping images sometimes 22650 // produces a green plus icon. When this happens the caretRangeFromPoint 22651 // will return "null" even though the x, y coordinate is correct. 22652 // But if we detach the insert from the drop event we will get a proper range 22653 window.setTimeout(function() { 22654 var pointRng = doc.caretRangeFromPoint(e.x, e.y); 22655 22656 if (dragStartRng) { 22657 selection.setRng(dragStartRng); 22658 dragStartRng = null; 22659 } 22660 22661 customDelete(); 22662 22663 selection.setRng(pointRng); 22664 editor.insertContent(internalContent); 22665 }, 0); 22666 } 22667 22668 } 22669 }); 22670 22671 editor.on('cut', function(e) { 22672 if (!isDefaultPrevented(e) && e.clipboardData) { 22673 e.preventDefault(); 22674 e.clipboardData.clearData(); 22675 e.clipboardData.setData('text/html', editor.selection.getContent()); 22676 e.clipboardData.setData('text/plain', editor.selection.getContent({format: 'text'})); 22677 customDelete(true); 22678 } 22679 }); 22680 } 22681 22682 /** 22683 * Makes sure that the editor body becomes empty when backspace or delete is pressed in empty editors. 22684 * 22685 * For example: 22686 * <p><b>|</b></p> 22687 * 22688 * Or: 22689 * <h1>|</h1> 22690 * 22691 * Or: 22692 * [<h1></h1>] 22693 */ 22694 function emptyEditorWhenDeleting() { 22695 function serializeRng(rng) { 22696 var body = dom.create("body"); 22697 var contents = rng.cloneContents(); 22698 body.appendChild(contents); 22699 return selection.serializer.serialize(body, {format: 'html'}); 22700 } 22701 22702 function allContentsSelected(rng) { 22703 if (!rng.setStart) { 22704 if (rng.item) { 22705 return false; 22706 } 22707 22708 var bodyRng = rng.duplicate(); 22709 bodyRng.moveToElementText(editor.getBody()); 22710 return RangeUtils.compareRanges(rng, bodyRng); 22711 } 22712 22713 var selection = serializeRng(rng); 22714 22715 var allRng = dom.createRng(); 22716 allRng.selectNode(editor.getBody()); 22717 22718 var allSelection = serializeRng(allRng); 22719 return selection === allSelection; 22720 } 22721 22722 editor.on('keydown', function(e) { 22723 var keyCode = e.keyCode, isCollapsed, body; 22724 22725 // Empty the editor if it's needed for example backspace at <p><b>|</b></p> 22726 if (!isDefaultPrevented(e) && (keyCode == DELETE || keyCode == BACKSPACE)) { 22727 isCollapsed = editor.selection.isCollapsed(); 22728 body = editor.getBody(); 22729 22730 // Selection is collapsed but the editor isn't empty 22731 if (isCollapsed && !dom.isEmpty(body)) { 22732 return; 22733 } 22734 22735 // Selection isn't collapsed but not all the contents is selected 22736 if (!isCollapsed && !allContentsSelected(editor.selection.getRng())) { 22737 return; 22738 } 22739 22740 // Manually empty the editor 22741 e.preventDefault(); 22742 editor.setContent(''); 22743 22744 if (body.firstChild && dom.isBlock(body.firstChild)) { 22745 editor.selection.setCursorLocation(body.firstChild, 0); 22746 } else { 22747 editor.selection.setCursorLocation(body, 0); 22748 } 22749 22750 editor.nodeChanged(); 22751 } 22752 }); 22753 } 22754 22755 /** 22756 * WebKit doesn't select all the nodes in the body when you press Ctrl+A. 22757 * IE selects more than the contents <body>[<p>a</p>]</body> instead of <body><p>[a]</p]</body> see bug #6438 22758 * This selects the whole body so that backspace/delete logic will delete everything 22759 */ 22760 function selectAll() { 22761 editor.on('keydown', function(e) { 22762 if (!isDefaultPrevented(e) && e.keyCode == 65 && VK.metaKeyPressed(e)) { 22763 e.preventDefault(); 22764 editor.execCommand('SelectAll'); 22765 } 22766 }); 22767 } 22768 22769 /** 22770 * WebKit has a weird issue where it some times fails to properly convert keypresses to input method keystrokes. 22771 * The IME on Mac doesn't initialize when it doesn't fire a proper focus event. 22772 * 22773 * This seems to happen when the user manages to click the documentElement element then the window doesn't get proper focus until 22774 * you enter a character into the editor. 22775 * 22776 * It also happens when the first focus in made to the body. 22777 * 22778 * See: https://bugs.webkit.org/show_bug.cgi?id=83566 22779 */ 22780 function inputMethodFocus() { 22781 if (!editor.settings.content_editable) { 22782 // Case 1 IME doesn't initialize if you focus the document 22783 dom.bind(editor.getDoc(), 'focusin', function() { 22784 selection.setRng(selection.getRng()); 22785 }); 22786 22787 // Case 2 IME doesn't initialize if you click the documentElement it also doesn't properly fire the focusin event 22788 dom.bind(editor.getDoc(), 'mousedown', function(e) { 22789 if (e.target == editor.getDoc().documentElement) { 22790 editor.getBody().focus(); 22791 selection.setRng(selection.getRng()); 22792 } 22793 }); 22794 } 22795 } 22796 22797 /** 22798 * Backspacing in FireFox/IE from a paragraph into a horizontal rule results in a floating text node because the 22799 * browser just deletes the paragraph - the browser fails to merge the text node with a horizontal rule so it is 22800 * left there. TinyMCE sees a floating text node and wraps it in a paragraph on the key up event (ForceBlocks.js 22801 * addRootBlocks), meaning the action does nothing. With this code, FireFox/IE matche the behaviour of other 22802 * browsers. 22803 * 22804 * It also fixes a bug on Firefox where it's impossible to delete HR elements. 22805 */ 22806 function removeHrOnBackspace() { 22807 editor.on('keydown', function(e) { 22808 if (!isDefaultPrevented(e) && e.keyCode === BACKSPACE) { 22809 if (selection.isCollapsed() && selection.getRng(true).startOffset === 0) { 22810 var node = selection.getNode(); 22811 var previousSibling = node.previousSibling; 22812 22813 if (node.nodeName == 'HR') { 22814 dom.remove(node); 22815 e.preventDefault(); 22816 return; 22817 } 22818 22819 if (previousSibling && previousSibling.nodeName && previousSibling.nodeName.toLowerCase() === "hr") { 22820 dom.remove(previousSibling); 22821 e.preventDefault(); 22822 } 22823 } 22824 } 22825 }); 22826 } 22827 22828 /** 22829 * Firefox 3.x has an issue where the body element won't get proper focus if you click out 22830 * side it's rectangle. 22831 */ 22832 function focusBody() { 22833 // Fix for a focus bug in FF 3.x where the body element 22834 // wouldn't get proper focus if the user clicked on the HTML element 22835 if (!window.Range.prototype.getClientRects) { // Detect getClientRects got introduced in FF 4 22836 editor.on('mousedown', function(e) { 22837 if (!isDefaultPrevented(e) && e.target.nodeName === "HTML") { 22838 var body = editor.getBody(); 22839 22840 // Blur the body it's focused but not correctly focused 22841 body.blur(); 22842 22843 // Refocus the body after a little while 22844 setTimeout(function() { 22845 body.focus(); 22846 }, 0); 22847 } 22848 }); 22849 } 22850 } 22851 22852 /** 22853 * WebKit has a bug where it isn't possible to select image, hr or anchor elements 22854 * by clicking on them so we need to fake that. 22855 */ 22856 function selectControlElements() { 22857 editor.on('click', function(e) { 22858 e = e.target; 22859 22860 // Workaround for bug, http://bugs.webkit.org/show_bug.cgi?id=12250 22861 // WebKit can't even do simple things like selecting an image 22862 // Needs tobe the setBaseAndExtend or it will fail to select floated images 22863 if (/^(IMG|HR)$/.test(e.nodeName)) { 22864 selection.getSel().setBaseAndExtent(e, 0, e, 1); 22865 } 22866 22867 if (e.nodeName == 'A' && dom.hasClass(e, 'mce-item-anchor')) { 22868 selection.select(e); 22869 } 22870 22871 editor.nodeChanged(); 22872 }); 22873 } 22874 22875 /** 22876 * Fixes a Gecko bug where the style attribute gets added to the wrong element when deleting between two block elements. 22877 * 22878 * Fixes do backspace/delete on this: 22879 * <p>bla[ck</p><p style="color:red">r]ed</p> 22880 * 22881 * Would become: 22882 * <p>bla|ed</p> 22883 * 22884 * Instead of: 22885 * <p style="color:red">bla|ed</p> 22886 */ 22887 function removeStylesWhenDeletingAcrossBlockElements() { 22888 function getAttributeApplyFunction() { 22889 var template = dom.getAttribs(selection.getStart().cloneNode(false)); 22890 22891 return function() { 22892 var target = selection.getStart(); 22893 22894 if (target !== editor.getBody()) { 22895 dom.setAttrib(target, "style", null); 22896 22897 each(template, function(attr) { 22898 target.setAttributeNode(attr.cloneNode(true)); 22899 }); 22900 } 22901 }; 22902 } 22903 22904 function isSelectionAcrossElements() { 22905 return !selection.isCollapsed() && 22906 dom.getParent(selection.getStart(), dom.isBlock) != dom.getParent(selection.getEnd(), dom.isBlock); 22907 } 22908 22909 editor.on('keypress', function(e) { 22910 var applyAttributes; 22911 22912 if (!isDefaultPrevented(e) && (e.keyCode == 8 || e.keyCode == 46) && isSelectionAcrossElements()) { 22913 applyAttributes = getAttributeApplyFunction(); 22914 editor.getDoc().execCommand('delete', false, null); 22915 applyAttributes(); 22916 e.preventDefault(); 22917 return false; 22918 } 22919 }); 22920 22921 dom.bind(editor.getDoc(), 'cut', function(e) { 22922 var applyAttributes; 22923 22924 if (!isDefaultPrevented(e) && isSelectionAcrossElements()) { 22925 applyAttributes = getAttributeApplyFunction(); 22926 22927 setTimeout(function() { 22928 applyAttributes(); 22929 }, 0); 22930 } 22931 }); 22932 } 22933 22934 /** 22935 * Fire a nodeChanged when the selection is changed on WebKit this fixes selection issues on iOS5. It only fires the nodeChange 22936 * event every 50ms since it would other wise update the UI when you type and it hogs the CPU. 22937 */ 22938 function selectionChangeNodeChanged() { 22939 var lastRng, selectionTimer; 22940 22941 editor.on('selectionchange', function() { 22942 if (selectionTimer) { 22943 clearTimeout(selectionTimer); 22944 selectionTimer = 0; 22945 } 22946 22947 selectionTimer = window.setTimeout(function() { 22948 if (editor.removed) { 22949 return; 22950 } 22951 22952 var rng = selection.getRng(); 22953 22954 // Compare the ranges to see if it was a real change or not 22955 if (!lastRng || !RangeUtils.compareRanges(rng, lastRng)) { 22956 editor.nodeChanged(); 22957 lastRng = rng; 22958 } 22959 }, 50); 22960 }); 22961 } 22962 22963 /** 22964 * Screen readers on IE needs to have the role application set on the body. 22965 */ 22966 function ensureBodyHasRoleApplication() { 22967 document.body.setAttribute("role", "application"); 22968 } 22969 22970 /** 22971 * Backspacing into a table behaves differently depending upon browser type. 22972 * Therefore, disable Backspace when cursor immediately follows a table. 22973 */ 22974 function disableBackspaceIntoATable() { 22975 editor.on('keydown', function(e) { 22976 if (!isDefaultPrevented(e) && e.keyCode === BACKSPACE) { 22977 if (selection.isCollapsed() && selection.getRng(true).startOffset === 0) { 22978 var previousSibling = selection.getNode().previousSibling; 22979 if (previousSibling && previousSibling.nodeName && previousSibling.nodeName.toLowerCase() === "table") { 22980 e.preventDefault(); 22981 return false; 22982 } 22983 } 22984 } 22985 }); 22986 } 22987 22988 /** 22989 * Old IE versions can't properly render BR elements in PRE tags white in contentEditable mode. So this 22990 * logic adds a \n before the BR so that it will get rendered. 22991 */ 22992 function addNewLinesBeforeBrInPre() { 22993 // IE8+ rendering mode does the right thing with BR in PRE 22994 if (getDocumentMode() > 7) { 22995 return; 22996 } 22997 22998 // Enable display: none in area and add a specific class that hides all BR elements in PRE to 22999 // avoid the caret from getting stuck at the BR elements while pressing the right arrow key 23000 setEditorCommandState('RespectVisibilityInDesign', true); 23001 editor.contentStyles.push('.mceHideBrInPre pre br {display: none}'); 23002 dom.addClass(editor.getBody(), 'mceHideBrInPre'); 23003 23004 // Adds a \n before all BR elements in PRE to get them visual 23005 parser.addNodeFilter('pre', function(nodes) { 23006 var i = nodes.length, brNodes, j, brElm, sibling; 23007 23008 while (i--) { 23009 brNodes = nodes[i].getAll('br'); 23010 j = brNodes.length; 23011 while (j--) { 23012 brElm = brNodes[j]; 23013 23014 // Add \n before BR in PRE elements on older IE:s so the new lines get rendered 23015 sibling = brElm.prev; 23016 if (sibling && sibling.type === 3 && sibling.value.charAt(sibling.value - 1) != '\n') { 23017 sibling.value += '\n'; 23018 } else { 23019 brElm.parent.insert(new Node('#text', 3), brElm, true).value = '\n'; 23020 } 23021 } 23022 } 23023 }); 23024 23025 // Removes any \n before BR elements in PRE since other browsers and in contentEditable=false mode they will be visible 23026 serializer.addNodeFilter('pre', function(nodes) { 23027 var i = nodes.length, brNodes, j, brElm, sibling; 23028 23029 while (i--) { 23030 brNodes = nodes[i].getAll('br'); 23031 j = brNodes.length; 23032 while (j--) { 23033 brElm = brNodes[j]; 23034 sibling = brElm.prev; 23035 if (sibling && sibling.type == 3) { 23036 sibling.value = sibling.value.replace(/\r?\n$/, ''); 23037 } 23038 } 23039 } 23040 }); 23041 } 23042 23043 /** 23044 * Moves style width/height to attribute width/height when the user resizes an image on IE. 23045 */ 23046 function removePreSerializedStylesWhenSelectingControls() { 23047 dom.bind(editor.getBody(), 'mouseup', function() { 23048 var value, node = selection.getNode(); 23049 23050 // Moved styles to attributes on IMG eements 23051 if (node.nodeName == 'IMG') { 23052 // Convert style width to width attribute 23053 if ((value = dom.getStyle(node, 'width'))) { 23054 dom.setAttrib(node, 'width', value.replace(/[^0-9%]+/g, '')); 23055 dom.setStyle(node, 'width', ''); 23056 } 23057 23058 // Convert style height to height attribute 23059 if ((value = dom.getStyle(node, 'height'))) { 23060 dom.setAttrib(node, 'height', value.replace(/[^0-9%]+/g, '')); 23061 dom.setStyle(node, 'height', ''); 23062 } 23063 } 23064 }); 23065 } 23066 23067 /** 23068 * Removes a blockquote when backspace is pressed at the beginning of it. 23069 * 23070 * For example: 23071 * <blockquote><p>|x</p></blockquote> 23072 * 23073 * Becomes: 23074 * <p>|x</p> 23075 */ 23076 function removeBlockQuoteOnBackSpace() { 23077 // Add block quote deletion handler 23078 editor.on('keydown', function(e) { 23079 var rng, container, offset, root, parent; 23080 23081 if (isDefaultPrevented(e) || e.keyCode != VK.BACKSPACE) { 23082 return; 23083 } 23084 23085 rng = selection.getRng(); 23086 container = rng.startContainer; 23087 offset = rng.startOffset; 23088 root = dom.getRoot(); 23089 parent = container; 23090 23091 if (!rng.collapsed || offset !== 0) { 23092 return; 23093 } 23094 23095 while (parent && parent.parentNode && parent.parentNode.firstChild == parent && parent.parentNode != root) { 23096 parent = parent.parentNode; 23097 } 23098 23099 // Is the cursor at the beginning of a blockquote? 23100 if (parent.tagName === 'BLOCKQUOTE') { 23101 // Remove the blockquote 23102 editor.formatter.toggle('blockquote', null, parent); 23103 23104 // Move the caret to the beginning of container 23105 rng = dom.createRng(); 23106 rng.setStart(container, 0); 23107 rng.setEnd(container, 0); 23108 selection.setRng(rng); 23109 } 23110 }); 23111 } 23112 23113 /** 23114 * Sets various Gecko editing options on mouse down and before a execCommand to disable inline table editing that is broken etc. 23115 */ 23116 function setGeckoEditingOptions() { 23117 function setOpts() { 23118 editor._refreshContentEditable(); 23119 23120 setEditorCommandState("StyleWithCSS", false); 23121 setEditorCommandState("enableInlineTableEditing", false); 23122 23123 if (!settings.object_resizing) { 23124 setEditorCommandState("enableObjectResizing", false); 23125 } 23126 } 23127 23128 if (!settings.readonly) { 23129 editor.on('BeforeExecCommand MouseDown', setOpts); 23130 } 23131 } 23132 23133 /** 23134 * Fixes a gecko link bug, when a link is placed at the end of block elements there is 23135 * no way to move the caret behind the link. This fix adds a bogus br element after the link. 23136 * 23137 * For example this: 23138 * <p><b><a href="#">x</a></b></p> 23139 * 23140 * Becomes this: 23141 * <p><b><a href="#">x</a></b><br></p> 23142 */ 23143 function addBrAfterLastLinks() { 23144 function fixLinks() { 23145 each(dom.select('a'), function(node) { 23146 var parentNode = node.parentNode, root = dom.getRoot(); 23147 23148 if (parentNode.lastChild === node) { 23149 while (parentNode && !dom.isBlock(parentNode)) { 23150 if (parentNode.parentNode.lastChild !== parentNode || parentNode === root) { 23151 return; 23152 } 23153 23154 parentNode = parentNode.parentNode; 23155 } 23156 23157 dom.add(parentNode, 'br', {'data-mce-bogus': 1}); 23158 } 23159 }); 23160 } 23161 23162 editor.on('SetContent ExecCommand', function(e) { 23163 if (e.type == "setcontent" || e.command === 'mceInsertLink') { 23164 fixLinks(); 23165 } 23166 }); 23167 } 23168 23169 /** 23170 * WebKit will produce DIV elements here and there by default. But since TinyMCE uses paragraphs by 23171 * default we want to change that behavior. 23172 */ 23173 function setDefaultBlockType() { 23174 if (settings.forced_root_block) { 23175 editor.on('init', function() { 23176 setEditorCommandState('DefaultParagraphSeparator', settings.forced_root_block); 23177 }); 23178 } 23179 } 23180 23181 /** 23182 * Removes ghost selections from images/tables on Gecko. 23183 */ 23184 function removeGhostSelection() { 23185 editor.on('Undo Redo SetContent', function(e) { 23186 if (!e.initial) { 23187 editor.execCommand('mceRepaint'); 23188 } 23189 }); 23190 } 23191 23192 /** 23193 * Deletes the selected image on IE instead of navigating to previous page. 23194 */ 23195 function deleteControlItemOnBackSpace() { 23196 editor.on('keydown', function(e) { 23197 var rng; 23198 23199 if (!isDefaultPrevented(e) && e.keyCode == BACKSPACE) { 23200 rng = editor.getDoc().selection.createRange(); 23201 if (rng && rng.item) { 23202 e.preventDefault(); 23203 editor.undoManager.beforeChange(); 23204 dom.remove(rng.item(0)); 23205 editor.undoManager.add(); 23206 } 23207 } 23208 }); 23209 } 23210 23211 /** 23212 * IE10 doesn't properly render block elements with the right height until you add contents to them. 23213 * This fixes that by adding a padding-right to all empty text block elements. 23214 * See: https://connect.microsoft.com/IE/feedback/details/743881 23215 */ 23216 function renderEmptyBlocksFix() { 23217 var emptyBlocksCSS; 23218 23219 // IE10+ 23220 if (getDocumentMode() >= 10) { 23221 emptyBlocksCSS = ''; 23222 each('p div h1 h2 h3 h4 h5 h6'.split(' '), function(name, i) { 23223 emptyBlocksCSS += (i > 0 ? ',' : '') + name + ':empty'; 23224 }); 23225 23226 editor.contentStyles.push(emptyBlocksCSS + '{padding-right: 1px !important}'); 23227 } 23228 } 23229 23230 /** 23231 * Old IE versions can't retain contents within noscript elements so this logic will store the contents 23232 * as a attribute and the insert that value as it's raw text when the DOM is serialized. 23233 */ 23234 function keepNoScriptContents() { 23235 if (getDocumentMode() < 9) { 23236 parser.addNodeFilter('noscript', function(nodes) { 23237 var i = nodes.length, node, textNode; 23238 23239 while (i--) { 23240 node = nodes[i]; 23241 textNode = node.firstChild; 23242 23243 if (textNode) { 23244 node.attr('data-mce-innertext', textNode.value); 23245 } 23246 } 23247 }); 23248 23249 serializer.addNodeFilter('noscript', function(nodes) { 23250 var i = nodes.length, node, textNode, value; 23251 23252 while (i--) { 23253 node = nodes[i]; 23254 textNode = nodes[i].firstChild; 23255 23256 if (textNode) { 23257 textNode.value = Entities.decode(textNode.value); 23258 } else { 23259 // Old IE can't retain noscript value so an attribute is used to store it 23260 value = node.attributes.map['data-mce-innertext']; 23261 if (value) { 23262 node.attr('data-mce-innertext', null); 23263 textNode = new Node('#text', 3); 23264 textNode.value = value; 23265 textNode.raw = true; 23266 node.append(textNode); 23267 } 23268 } 23269 } 23270 }); 23271 } 23272 } 23273 23274 /** 23275 * IE has an issue where you can't select/move the caret by clicking outside the body if the document is in standards mode. 23276 */ 23277 function fixCaretSelectionOfDocumentElementOnIe() { 23278 var doc = dom.doc, body = doc.body, started, startRng, htmlElm; 23279 23280 // Return range from point or null if it failed 23281 function rngFromPoint(x, y) { 23282 var rng = body.createTextRange(); 23283 23284 try { 23285 rng.moveToPoint(x, y); 23286 } catch (ex) { 23287 // IE sometimes throws and exception, so lets just ignore it 23288 rng = null; 23289 } 23290 23291 return rng; 23292 } 23293 23294 // Fires while the selection is changing 23295 function selectionChange(e) { 23296 var pointRng; 23297 23298 // Check if the button is down or not 23299 if (e.button) { 23300 // Create range from mouse position 23301 pointRng = rngFromPoint(e.x, e.y); 23302 23303 if (pointRng) { 23304 // Check if pointRange is before/after selection then change the endPoint 23305 if (pointRng.compareEndPoints('StartToStart', startRng) > 0) { 23306 pointRng.setEndPoint('StartToStart', startRng); 23307 } else { 23308 pointRng.setEndPoint('EndToEnd', startRng); 23309 } 23310 23311 pointRng.select(); 23312 } 23313 } else { 23314 endSelection(); 23315 } 23316 } 23317 23318 // Removes listeners 23319 function endSelection() { 23320 var rng = doc.selection.createRange(); 23321 23322 // If the range is collapsed then use the last start range 23323 if (startRng && !rng.item && rng.compareEndPoints('StartToEnd', rng) === 0) { 23324 startRng.select(); 23325 } 23326 23327 dom.unbind(doc, 'mouseup', endSelection); 23328 dom.unbind(doc, 'mousemove', selectionChange); 23329 startRng = started = 0; 23330 } 23331 23332 // Make HTML element unselectable since we are going to handle selection by hand 23333 doc.documentElement.unselectable = true; 23334 23335 // Detect when user selects outside BODY 23336 dom.bind(doc, 'mousedown contextmenu', function(e) { 23337 if (e.target.nodeName === 'HTML') { 23338 if (started) { 23339 endSelection(); 23340 } 23341 23342 // Detect vertical scrollbar, since IE will fire a mousedown on the scrollbar and have target set as HTML 23343 htmlElm = doc.documentElement; 23344 if (htmlElm.scrollHeight > htmlElm.clientHeight) { 23345 return; 23346 } 23347 23348 started = 1; 23349 // Setup start position 23350 startRng = rngFromPoint(e.x, e.y); 23351 if (startRng) { 23352 // Listen for selection change events 23353 dom.bind(doc, 'mouseup', endSelection); 23354 dom.bind(doc, 'mousemove', selectionChange); 23355 23356 dom.getRoot().focus(); 23357 startRng.select(); 23358 } 23359 } 23360 }); 23361 } 23362 23363 /** 23364 * Fixes selection issues where the caret can be placed between two inline elements like <b>a</b>|<b>b</b> 23365 * this fix will lean the caret right into the closest inline element. 23366 */ 23367 function normalizeSelection() { 23368 // Normalize selection for example <b>a</b><i>|a</i> becomes <b>a|</b><i>a</i> except for Ctrl+A since it selects everything 23369 editor.on('keyup focusin mouseup', function(e) { 23370 if (e.keyCode != 65 || !VK.metaKeyPressed(e)) { 23371 selection.normalize(); 23372 } 23373 }, true); 23374 } 23375 23376 /** 23377 * Forces Gecko to render a broken image icon if it fails to load an image. 23378 */ 23379 function showBrokenImageIcon() { 23380 editor.contentStyles.push( 23381 'img:-moz-broken {' + 23382 '-moz-force-broken-image-icon:1;' + 23383 'min-width:24px;' + 23384 'min-height:24px' + 23385 '}' 23386 ); 23387 } 23388 23389 /** 23390 * iOS has a bug where it's impossible to type if the document has a touchstart event 23391 * bound and the user touches the document while having the on screen keyboard visible. 23392 * 23393 * The touch event moves the focus to the parent document while having the caret inside the iframe 23394 * this fix moves the focus back into the iframe document. 23395 */ 23396 function restoreFocusOnKeyDown() { 23397 if (!editor.inline) { 23398 editor.on('keydown', function() { 23399 if (document.activeElement == document.body) { 23400 editor.getWin().focus(); 23401 } 23402 }); 23403 } 23404 } 23405 23406 /** 23407 * IE 11 has an annoying issue where you can't move focus into the editor 23408 * by clicking on the white area HTML element. We used to be able to to fix this with 23409 * the fixCaretSelectionOfDocumentElementOnIe fix. But since M$ removed the selection 23410 * object it's not possible anymore. So we need to hack in a ungly CSS to force the 23411 * body to be at least 150px. If the user clicks the HTML element out side this 150px region 23412 * we simply move the focus into the first paragraph. Not ideal since you loose the 23413 * positioning of the caret but goot enough for most cases. 23414 */ 23415 function bodyHeight() { 23416 if (!editor.inline) { 23417 editor.contentStyles.push('body {min-height: 150px}'); 23418 editor.on('click', function(e) { 23419 if (e.target.nodeName == 'HTML') { 23420 editor.getBody().focus(); 23421 editor.selection.normalize(); 23422 editor.nodeChanged(); 23423 } 23424 }); 23425 } 23426 } 23427 23428 /** 23429 * Firefox on Mac OS will move the browser back to the previous page if you press CMD+Left arrow. 23430 * You might then loose all your work so we need to block that behavior and replace it with our own. 23431 */ 23432 function blockCmdArrowNavigation() { 23433 if (Env.mac) { 23434 editor.on('keydown', function(e) { 23435 if (VK.metaKeyPressed(e) && (e.keyCode == 37 || e.keyCode == 39)) { 23436 e.preventDefault(); 23437 editor.selection.getSel().modify('move', e.keyCode == 37 ? 'backward' : 'forward', 'word'); 23438 } 23439 }); 23440 } 23441 } 23442 23443 /** 23444 * Disables the autolinking in IE 9+ this is then re-enabled by the autolink plugin. 23445 */ 23446 function disableAutoUrlDetect() { 23447 setEditorCommandState("AutoUrlDetect", false); 23448 } 23449 23450 /** 23451 * IE 11 has a fantastic bug where it will produce two trailing BR elements to iframe bodies when 23452 * the iframe is hidden by display: none on a parent container. The DOM is actually out of sync 23453 * with innerHTML in this case. It's like IE adds shadow DOM BR elements that appears on innerHTML 23454 * but not as the lastChild of the body. However is we add a BR element to the body then remove it 23455 * it doesn't seem to add these BR elements makes sence right?! 23456 * 23457 * Example of what happens: <body>text</body> becomes <body>text<br><br></body> 23458 */ 23459 function doubleTrailingBrElements() { 23460 if (!editor.inline) { 23461 editor.on('focus blur beforegetcontent', function() { 23462 var br = editor.dom.create('br'); 23463 editor.getBody().appendChild(br); 23464 br.parentNode.removeChild(br); 23465 }, true); 23466 } 23467 } 23468 23469 /** 23470 * iOS 7.1 introduced two new bugs: 23471 * 1) It's possible to open links within a contentEditable area by clicking on them. 23472 * 2) If you hold down the finger it will display the link/image touch callout menu. 23473 */ 23474 function tapLinksAndImages() { 23475 editor.on('click', function(e) { 23476 var elm = e.target; 23477 23478 do { 23479 if (elm.tagName === 'A') { 23480 e.preventDefault(); 23481 return; 23482 } 23483 } while ((elm = elm.parentNode)); 23484 }); 23485 23486 editor.contentStyles.push('.mce-content-body {-webkit-touch-callout: none}'); 23487 } 23488 23489 /** 23490 * WebKit has a bug where it will allow forms to be submitted if they are inside a contentEditable element. 23491 * For example this: <form><button></form> 23492 */ 23493 function blockFormSubmitInsideEditor() { 23494 editor.on('init', function() { 23495 editor.dom.bind(editor.getBody(), 'submit', function(e) { 23496 e.preventDefault(); 23497 }); 23498 }); 23499 } 23500 23501 // All browsers 23502 disableBackspaceIntoATable(); 23503 removeBlockQuoteOnBackSpace(); 23504 emptyEditorWhenDeleting(); 23505 normalizeSelection(); 23506 23507 // WebKit 23508 if (isWebKit) { 23509 cleanupStylesWhenDeleting(); 23510 inputMethodFocus(); 23511 selectControlElements(); 23512 setDefaultBlockType(); 23513 blockFormSubmitInsideEditor(); 23514 23515 // iOS 23516 if (Env.iOS) { 23517 selectionChangeNodeChanged(); 23518 restoreFocusOnKeyDown(); 23519 bodyHeight(); 23520 tapLinksAndImages(); 23521 } else { 23522 selectAll(); 23523 } 23524 } 23525 23526 // IE 23527 if (isIE && Env.ie < 11) { 23528 removeHrOnBackspace(); 23529 ensureBodyHasRoleApplication(); 23530 addNewLinesBeforeBrInPre(); 23531 removePreSerializedStylesWhenSelectingControls(); 23532 deleteControlItemOnBackSpace(); 23533 renderEmptyBlocksFix(); 23534 keepNoScriptContents(); 23535 fixCaretSelectionOfDocumentElementOnIe(); 23536 } 23537 23538 if (Env.ie >= 11) { 23539 bodyHeight(); 23540 doubleTrailingBrElements(); 23541 } 23542 23543 if (Env.ie) { 23544 selectAll(); 23545 disableAutoUrlDetect(); 23546 } 23547 23548 // Gecko 23549 if (isGecko) { 23550 removeHrOnBackspace(); 23551 focusBody(); 23552 removeStylesWhenDeletingAcrossBlockElements(); 23553 setGeckoEditingOptions(); 23554 addBrAfterLastLinks(); 23555 removeGhostSelection(); 23556 showBrokenImageIcon(); 23557 blockCmdArrowNavigation(); 23558 } 23559 }; 23560 }); 23561 23562 // Included from: js/tinymce/classes/util/Observable.js 23563 23564 /** 23565 * Observable.js 23566 * 23567 * Copyright, Moxiecode Systems AB 23568 * Released under LGPL License. 23569 * 23570 * License: http://www.tinymce.com/license 23571 * Contributing: http://www.tinymce.com/contributing 23572 */ 23573 23574 /** 23575 * This mixin will add event binding logic to classes. 23576 * 23577 * @mixin tinymce.util.Observable 23578 */ 23579 define("tinymce/util/Observable", [ 23580 "tinymce/util/EventDispatcher" 23581 ], function(EventDispatcher) { 23582 function getEventDispatcher(obj) { 23583 if (!obj._eventDispatcher) { 23584 obj._eventDispatcher = new EventDispatcher({ 23585 scope: obj, 23586 toggleEvent: function(name, state) { 23587 if (EventDispatcher.isNative(name) && obj.toggleNativeEvent) { 23588 obj.toggleNativeEvent(name, state); 23589 } 23590 } 23591 }); 23592 } 23593 23594 return obj._eventDispatcher; 23595 } 23596 23597 return { 23598 /** 23599 * Fires the specified event by name. 23600 * 23601 * @method fire 23602 * @param {String} name Name of the event to fire. 23603 * @param {Object?} args Event arguments. 23604 * @param {Boolean?} bubble True/false if the event is to be bubbled. 23605 * @return {Object} Event args instance passed in. 23606 * @example 23607 * instance.fire('event', {...}); 23608 */ 23609 fire: function(name, args, bubble) { 23610 var self = this; 23611 23612 // Prevent all events except the remove event after the instance has been removed 23613 if (self.removed && name !== "remove") { 23614 return args; 23615 } 23616 23617 args = getEventDispatcher(self).fire(name, args, bubble); 23618 23619 // Bubble event up to parents 23620 if (bubble !== false && self.parent) { 23621 var parent = self.parent(); 23622 while (parent && !args.isPropagationStopped()) { 23623 parent.fire(name, args, false); 23624 parent = parent.parent(); 23625 } 23626 } 23627 23628 return args; 23629 }, 23630 23631 /** 23632 * Binds an event listener to a specific event by name. 23633 * 23634 * @method on 23635 * @param {String} name Event name or space separated list of events to bind. 23636 * @param {callback} callback Callback to be executed when the event occurs. 23637 * @param {Boolean} first Optional flag if the event should be prepended. Use this with care. 23638 * @return {Object} Current class instance. 23639 * @example 23640 * instance.on('event', function(e) { 23641 * // Callback logic 23642 * }); 23643 */ 23644 on: function(name, callback, prepend) { 23645 return getEventDispatcher(this).on(name, callback, prepend); 23646 }, 23647 23648 /** 23649 * Unbinds an event listener to a specific event by name. 23650 * 23651 * @method off 23652 * @param {String?} name Name of the event to unbind. 23653 * @param {callback?} callback Callback to unbind. 23654 * @return {Object} Current class instance. 23655 * @example 23656 * // Unbind specific callback 23657 * instance.off('event', handler); 23658 * 23659 * // Unbind all listeners by name 23660 * instance.off('event'); 23661 * 23662 * // Unbind all events 23663 * instance.off(); 23664 */ 23665 off: function(name, callback) { 23666 return getEventDispatcher(this).off(name, callback); 23667 }, 23668 23669 /** 23670 * Returns true/false if the object has a event of the specified name. 23671 * 23672 * @method hasEventListeners 23673 * @param {String} name Name of the event to check for. 23674 * @return {Boolean} true/false if the event exists or not. 23675 */ 23676 hasEventListeners: function(name) { 23677 return getEventDispatcher(this).has(name); 23678 } 23679 }; 23680 }); 23681 23682 // Included from: js/tinymce/classes/EditorObservable.js 23683 23684 /** 23685 * EditorObservable.js 23686 * 23687 * Copyright, Moxiecode Systems AB 23688 * Released under LGPL License. 23689 * 23690 * License: http://www.tinymce.com/license 23691 * Contributing: http://www.tinymce.com/contributing 23692 */ 23693 23694 /** 23695 * This mixin contains the event logic for the tinymce.Editor class. 23696 * 23697 * @mixin tinymce.EditorObservable 23698 * @extends tinymce.util.Observable 23699 */ 23700 define("tinymce/EditorObservable", [ 23701 "tinymce/util/Observable", 23702 "tinymce/dom/DOMUtils", 23703 "tinymce/util/Tools" 23704 ], function(Observable, DOMUtils, Tools) { 23705 var DOM = DOMUtils.DOM; 23706 23707 function getEventTarget(editor, eventName) { 23708 if (eventName == 'selectionchange') { 23709 return editor.getDoc(); 23710 } 23711 23712 // Need to bind mousedown/mouseup etc to document not body in iframe mode 23713 // Since the user might click on the HTML element not the BODY 23714 if (!editor.inline && /^mouse|click|contextmenu|drop/.test(eventName)) { 23715 return editor.getDoc(); 23716 } 23717 23718 return editor.getBody(); 23719 } 23720 23721 function bindEventDelegate(editor, name) { 23722 var eventRootSelector = editor.settings.event_root, editorManager = editor.editorManager; 23723 var eventRootElm = editorManager.eventRootElm || getEventTarget(editor, name); 23724 23725 if (eventRootSelector) { 23726 if (!editorManager.rootEvents) { 23727 editorManager.rootEvents = {}; 23728 23729 editorManager.on('RemoveEditor', function() { 23730 if (!editorManager.activeEditor) { 23731 DOM.unbind(eventRootElm); 23732 delete editorManager.rootEvents; 23733 } 23734 }); 23735 } 23736 23737 if (editorManager.rootEvents[name]) { 23738 return; 23739 } 23740 23741 if (eventRootElm == editor.getBody()) { 23742 eventRootElm = DOM.select(eventRootSelector)[0]; 23743 editorManager.eventRootElm = eventRootElm; 23744 } 23745 23746 editorManager.rootEvents[name] = true; 23747 23748 DOM.bind(eventRootElm, name, function(e) { 23749 var target = e.target, editors = editorManager.editors, i = editors.length; 23750 23751 while (i--) { 23752 var body = editors[i].getBody(); 23753 23754 if (body === target || DOM.isChildOf(target, body)) { 23755 if (!editors[i].hidden) { 23756 editors[i].fire(name, e); 23757 } 23758 } 23759 } 23760 }); 23761 } else { 23762 editor.dom.bind(eventRootElm, name, function(e) { 23763 if (!editor.hidden) { 23764 editor.fire(name, e); 23765 } 23766 }); 23767 } 23768 } 23769 23770 var EditorObservable = { 23771 bindPendingEventDelegates: function() { 23772 var self = this; 23773 23774 Tools.each(self._pendingNativeEvents, function(name) { 23775 bindEventDelegate(self, name); 23776 }); 23777 }, 23778 23779 toggleNativeEvent: function(name, state) { 23780 var self = this; 23781 23782 if (self.settings.readonly) { 23783 return; 23784 } 23785 23786 // Never bind focus/blur since the FocusManager fakes those 23787 if (name == "focus" || name == "blur") { 23788 return; 23789 } 23790 23791 if (state) { 23792 if (self.initialized) { 23793 bindEventDelegate(self, name); 23794 } else { 23795 if (!self._pendingNativeEvents) { 23796 self._pendingNativeEvents = [name]; 23797 } else { 23798 self._pendingNativeEvents.push(name); 23799 } 23800 } 23801 } else if (self.initialized) { 23802 self.dom.unbind(getEventTarget(self, name), name); 23803 } 23804 } 23805 }; 23806 23807 EditorObservable = Tools.extend({}, Observable, EditorObservable); 23808 23809 return EditorObservable; 23810 }); 23811 23812 // Included from: js/tinymce/classes/Shortcuts.js 23813 23814 /** 23815 * Shortcuts.js 23816 * 23817 * Copyright, Moxiecode Systems AB 23818 * Released under LGPL License. 23819 * 23820 * License: http://www.tinymce.com/license 23821 * Contributing: http://www.tinymce.com/contributing 23822 */ 23823 23824 /** 23825 * Contains all logic for handling of keyboard shortcuts. 23826 */ 23827 define("tinymce/Shortcuts", [ 23828 "tinymce/util/Tools", 23829 "tinymce/Env" 23830 ], function(Tools, Env) { 23831 var each = Tools.each, explode = Tools.explode; 23832 23833 var keyCodeLookup = { 23834 "f9": 120, 23835 "f10": 121, 23836 "f11": 122 23837 }; 23838 23839 return function(editor) { 23840 var self = this, shortcuts = {}; 23841 23842 editor.on('keyup keypress keydown', function(e) { 23843 if (e.altKey || e.ctrlKey || e.metaKey) { 23844 each(shortcuts, function(shortcut) { 23845 var ctrlKey = Env.mac ? e.metaKey : e.ctrlKey; 23846 23847 if (shortcut.ctrl != ctrlKey || shortcut.alt != e.altKey || shortcut.shift != e.shiftKey) { 23848 return; 23849 } 23850 23851 if (e.keyCode == shortcut.keyCode || (e.charCode && e.charCode == shortcut.charCode)) { 23852 e.preventDefault(); 23853 23854 if (e.type == "keydown") { 23855 shortcut.func.call(shortcut.scope); 23856 } 23857 23858 return true; 23859 } 23860 }); 23861 } 23862 }); 23863 23864 /** 23865 * Adds a keyboard shortcut for some command or function. 23866 * 23867 * @method addShortcut 23868 * @param {String} pattern Shortcut pattern. Like for example: ctrl+alt+o. 23869 * @param {String} desc Text description for the command. 23870 * @param {String/Function} cmdFunc Command name string or function to execute when the key is pressed. 23871 * @param {Object} sc Optional scope to execute the function in. 23872 * @return {Boolean} true/false state if the shortcut was added or not. 23873 */ 23874 self.add = function(pattern, desc, cmdFunc, scope) { 23875 var cmd; 23876 23877 cmd = cmdFunc; 23878 23879 if (typeof(cmdFunc) === 'string') { 23880 cmdFunc = function() { 23881 editor.execCommand(cmd, false, null); 23882 }; 23883 } else if (Tools.isArray(cmd)) { 23884 cmdFunc = function() { 23885 editor.execCommand(cmd[0], cmd[1], cmd[2]); 23886 }; 23887 } 23888 23889 each(explode(pattern.toLowerCase()), function(pattern) { 23890 var shortcut = { 23891 func: cmdFunc, 23892 scope: scope || editor, 23893 desc: editor.translate(desc), 23894 alt: false, 23895 ctrl: false, 23896 shift: false 23897 }; 23898 23899 each(explode(pattern, '+'), function(value) { 23900 switch (value) { 23901 case 'alt': 23902 case 'ctrl': 23903 case 'shift': 23904 shortcut[value] = true; 23905 break; 23906 23907 default: 23908 shortcut.charCode = value.charCodeAt(0); 23909 shortcut.keyCode = keyCodeLookup[value] || value.toUpperCase().charCodeAt(0); 23910 } 23911 }); 23912 23913 shortcuts[ 23914 (shortcut.ctrl ? 'ctrl' : '') + ',' + 23915 (shortcut.alt ? 'alt' : '') + ',' + 23916 (shortcut.shift ? 'shift' : '') + ',' + 23917 shortcut.keyCode 23918 ] = shortcut; 23919 }); 23920 23921 return true; 23922 }; 23923 }; 23924 }); 23925 23926 // Included from: js/tinymce/classes/Editor.js 23927 23928 /** 23929 * Editor.js 23930 * 23931 * Copyright, Moxiecode Systems AB 23932 * Released under LGPL License. 23933 * 23934 * License: http://www.tinymce.com/license 23935 * Contributing: http://www.tinymce.com/contributing 23936 */ 23937 23938 /*jshint scripturl:true */ 23939 23940 /** 23941 * Include the base event class documentation. 23942 * 23943 * @include ../../../tools/docs/tinymce.Event.js 23944 */ 23945 23946 /** 23947 * This class contains the core logic for a TinyMCE editor. 23948 * 23949 * @class tinymce.Editor 23950 * @mixes tinymce.util.Observable 23951 * @example 23952 * // Add a class to all paragraphs in the editor. 23953 * tinymce.activeEditor.dom.addClass(tinymce.activeEditor.dom.select('p'), 'someclass'); 23954 * 23955 * // Gets the current editors selection as text 23956 * tinymce.activeEditor.selection.getContent({format: 'text'}); 23957 * 23958 * // Creates a new editor instance 23959 * var ed = new tinymce.Editor('textareaid', { 23960 * some_setting: 1 23961 * }, tinymce.EditorManager); 23962 * 23963 * // Select each item the user clicks on 23964 * ed.on('click', function(e) { 23965 * ed.selection.select(e.target); 23966 * }); 23967 * 23968 * ed.render(); 23969 */ 23970 define("tinymce/Editor", [ 23971 "tinymce/dom/DOMUtils", 23972 "tinymce/AddOnManager", 23973 "tinymce/html/Node", 23974 "tinymce/dom/Serializer", 23975 "tinymce/html/Serializer", 23976 "tinymce/dom/Selection", 23977 "tinymce/Formatter", 23978 "tinymce/UndoManager", 23979 "tinymce/EnterKey", 23980 "tinymce/ForceBlocks", 23981 "tinymce/EditorCommands", 23982 "tinymce/util/URI", 23983 "tinymce/dom/ScriptLoader", 23984 "tinymce/dom/EventUtils", 23985 "tinymce/WindowManager", 23986 "tinymce/html/Schema", 23987 "tinymce/html/DomParser", 23988 "tinymce/util/Quirks", 23989 "tinymce/Env", 23990 "tinymce/util/Tools", 23991 "tinymce/EditorObservable", 23992 "tinymce/Shortcuts" 23993 ], function( 23994 DOMUtils, AddOnManager, Node, DomSerializer, Serializer, 23995 Selection, Formatter, UndoManager, EnterKey, ForceBlocks, EditorCommands, 23996 URI, ScriptLoader, EventUtils, WindowManager, 23997 Schema, DomParser, Quirks, Env, Tools, EditorObservable, Shortcuts 23998 ) { 23999 // Shorten these names 24000 var DOM = DOMUtils.DOM, ThemeManager = AddOnManager.ThemeManager, PluginManager = AddOnManager.PluginManager; 24001 var extend = Tools.extend, each = Tools.each, explode = Tools.explode; 24002 var inArray = Tools.inArray, trim = Tools.trim, resolve = Tools.resolve; 24003 var Event = EventUtils.Event; 24004 var isGecko = Env.gecko, ie = Env.ie; 24005 24006 /** 24007 * Include documentation for all the events. 24008 * 24009 * @include ../../../tools/docs/tinymce.Editor.js 24010 */ 24011 24012 /** 24013 * Constructs a editor instance by id. 24014 * 24015 * @constructor 24016 * @method Editor 24017 * @param {String} id Unique id for the editor. 24018 * @param {Object} settings Settings for the editor. 24019 * @param {tinymce.EditorManager} editorManager EditorManager instance. 24020 * @author Moxiecode 24021 */ 24022 function Editor(id, settings, editorManager) { 24023 var self = this, documentBaseUrl, baseUri; 24024 24025 documentBaseUrl = self.documentBaseUrl = editorManager.documentBaseURL; 24026 baseUri = editorManager.baseURI; 24027 24028 /** 24029 * Name/value collection with editor settings. 24030 * 24031 * @property settings 24032 * @type Object 24033 * @example 24034 * // Get the value of the theme setting 24035 * tinymce.activeEditor.windowManager.alert("You are using the " + tinymce.activeEditor.settings.theme + " theme"); 24036 */ 24037 self.settings = settings = extend({ 24038 id: id, 24039 theme: 'modern', 24040 delta_width: 0, 24041 delta_height: 0, 24042 popup_css: '', 24043 plugins: '', 24044 document_base_url: documentBaseUrl, 24045 add_form_submit_trigger: true, 24046 submit_patch: true, 24047 add_unload_trigger: true, 24048 convert_urls: true, 24049 relative_urls: true, 24050 remove_script_host: true, 24051 object_resizing: true, 24052 doctype: '<!DOCTYPE html>', 24053 visual: true, 24054 font_size_style_values: 'xx-small,x-small,small,medium,large,x-large,xx-large', 24055 24056 // See: http://www.w3.org/TR/CSS2/fonts.html#propdef-font-size 24057 font_size_legacy_values: 'xx-small,small,medium,large,x-large,xx-large,300%', 24058 forced_root_block: 'p', 24059 hidden_input: true, 24060 padd_empty_editor: true, 24061 render_ui: true, 24062 indentation: '30px', 24063 inline_styles: true, 24064 convert_fonts_to_spans: true, 24065 indent: 'simple', 24066 indent_before: 'p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,ul,li,area,table,thead,' + 24067 'tfoot,tbody,tr,section,article,hgroup,aside,figure,option,optgroup,datalist', 24068 indent_after: 'p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,ul,li,area,table,thead,' + 24069 'tfoot,tbody,tr,section,article,hgroup,aside,figure,option,optgroup,datalist', 24070 validate: true, 24071 entity_encoding: 'named', 24072 url_converter: self.convertURL, 24073 url_converter_scope: self, 24074 ie7_compat: true 24075 }, settings); 24076 24077 AddOnManager.language = settings.language || 'en'; 24078 AddOnManager.languageLoad = settings.language_load; 24079 24080 AddOnManager.baseURL = editorManager.baseURL; 24081 24082 /** 24083 * Editor instance id, normally the same as the div/textarea that was replaced. 24084 * 24085 * @property id 24086 * @type String 24087 */ 24088 self.id = settings.id = id; 24089 24090 /** 24091 * State to force the editor to return false on a isDirty call. 24092 * 24093 * @property isNotDirty 24094 * @type Boolean 24095 * @example 24096 * function ajaxSave() { 24097 * var ed = tinymce.get('elm1'); 24098 * 24099 * // Save contents using some XHR call 24100 * alert(ed.getContent()); 24101 * 24102 * ed.isNotDirty = true; // Force not dirty state 24103 * } 24104 */ 24105 self.isNotDirty = true; 24106 24107 /** 24108 * Name/Value object containting plugin instances. 24109 * 24110 * @property plugins 24111 * @type Object 24112 * @example 24113 * // Execute a method inside a plugin directly 24114 * tinymce.activeEditor.plugins.someplugin.someMethod(); 24115 */ 24116 self.plugins = {}; 24117 24118 /** 24119 * URI object to document configured for the TinyMCE instance. 24120 * 24121 * @property documentBaseURI 24122 * @type tinymce.util.URI 24123 * @example 24124 * // Get relative URL from the location of document_base_url 24125 * tinymce.activeEditor.documentBaseURI.toRelative('/somedir/somefile.htm'); 24126 * 24127 * // Get absolute URL from the location of document_base_url 24128 * tinymce.activeEditor.documentBaseURI.toAbsolute('somefile.htm'); 24129 */ 24130 self.documentBaseURI = new URI(settings.document_base_url || documentBaseUrl, { 24131 base_uri: baseUri 24132 }); 24133 24134 /** 24135 * URI object to current document that holds the TinyMCE editor instance. 24136 * 24137 * @property baseURI 24138 * @type tinymce.util.URI 24139 * @example 24140 * // Get relative URL from the location of the API 24141 * tinymce.activeEditor.baseURI.toRelative('/somedir/somefile.htm'); 24142 * 24143 * // Get absolute URL from the location of the API 24144 * tinymce.activeEditor.baseURI.toAbsolute('somefile.htm'); 24145 */ 24146 self.baseURI = baseUri; 24147 24148 /** 24149 * Array with CSS files to load into the iframe. 24150 * 24151 * @property contentCSS 24152 * @type Array 24153 */ 24154 self.contentCSS = []; 24155 24156 /** 24157 * Array of CSS styles to add to head of document when the editor loads. 24158 * 24159 * @property contentStyles 24160 * @type Array 24161 */ 24162 self.contentStyles = []; 24163 24164 // Creates all events like onClick, onSetContent etc see Editor.Events.js for the actual logic 24165 self.shortcuts = new Shortcuts(self); 24166 24167 // Internal command handler objects 24168 self.execCommands = {}; 24169 self.queryStateCommands = {}; 24170 self.queryValueCommands = {}; 24171 self.loadedCSS = {}; 24172 24173 self.suffix = editorManager.suffix; 24174 self.editorManager = editorManager; 24175 self.inline = settings.inline; 24176 24177 // Call setup 24178 editorManager.fire('SetupEditor', self); 24179 self.execCallback('setup', self); 24180 } 24181 24182 Editor.prototype = { 24183 /** 24184 * Renderes the editor/adds it to the page. 24185 * 24186 * @method render 24187 */ 24188 render: function() { 24189 var self = this, settings = self.settings, id = self.id, suffix = self.suffix; 24190 24191 function readyHandler() { 24192 DOM.unbind(window, 'ready', readyHandler); 24193 self.render(); 24194 } 24195 24196 // Page is not loaded yet, wait for it 24197 if (!Event.domLoaded) { 24198 DOM.bind(window, 'ready', readyHandler); 24199 return; 24200 } 24201 24202 // Element not found, then skip initialization 24203 if (!self.getElement()) { 24204 return; 24205 } 24206 24207 // No editable support old iOS versions etc 24208 if (!Env.contentEditable) { 24209 return; 24210 } 24211 24212 // Hide target element early to prevent content flashing 24213 if (!settings.inline) { 24214 self.orgVisibility = self.getElement().style.visibility; 24215 self.getElement().style.visibility = 'hidden'; 24216 } else { 24217 self.inline = true; 24218 } 24219 24220 var form = self.getElement().form || DOM.getParent(id, 'form'); 24221 if (form) { 24222 self.formElement = form; 24223 24224 // Add hidden input for non input elements inside form elements 24225 if (settings.hidden_input && !/TEXTAREA|INPUT/i.test(self.getElement().nodeName)) { 24226 DOM.insertAfter(DOM.create('input', {type: 'hidden', name: id}), id); 24227 self.hasHiddenInput = true; 24228 } 24229 24230 // Pass submit/reset from form to editor instance 24231 self.formEventDelegate = function(e) { 24232 self.fire(e.type, e); 24233 }; 24234 24235 DOM.bind(form, 'submit reset', self.formEventDelegate); 24236 24237 // Reset contents in editor when the form is reset 24238 self.on('reset', function() { 24239 self.setContent(self.startContent, {format: 'raw'}); 24240 }); 24241 24242 // Check page uses id="submit" or name="submit" for it's submit button 24243 if (settings.submit_patch && !form.submit.nodeType && !form.submit.length && !form._mceOldSubmit) { 24244 form._mceOldSubmit = form.submit; 24245 form.submit = function() { 24246 self.editorManager.triggerSave(); 24247 self.isNotDirty = true; 24248 24249 return form._mceOldSubmit(form); 24250 }; 24251 } 24252 } 24253 24254 /** 24255 * Window manager reference, use this to open new windows and dialogs. 24256 * 24257 * @property windowManager 24258 * @type tinymce.WindowManager 24259 * @example 24260 * // Shows an alert message 24261 * tinymce.activeEditor.windowManager.alert('Hello world!'); 24262 * 24263 * // Opens a new dialog with the file.htm file and the size 320x240 24264 * // It also adds a custom parameter this can be retrieved by using tinyMCEPopup.getWindowArg inside the dialog. 24265 * tinymce.activeEditor.windowManager.open({ 24266 * url: 'file.htm', 24267 * width: 320, 24268 * height: 240 24269 * }, { 24270 * custom_param: 1 24271 * }); 24272 */ 24273 self.windowManager = new WindowManager(self); 24274 24275 if (settings.encoding == 'xml') { 24276 self.on('GetContent', function(e) { 24277 if (e.save) { 24278 e.content = DOM.encode(e.content); 24279 } 24280 }); 24281 } 24282 24283 if (settings.add_form_submit_trigger) { 24284 self.on('submit', function() { 24285 if (self.initialized) { 24286 self.save(); 24287 } 24288 }); 24289 } 24290 24291 if (settings.add_unload_trigger) { 24292 self._beforeUnload = function() { 24293 if (self.initialized && !self.destroyed && !self.isHidden()) { 24294 self.save({format: 'raw', no_events: true, set_dirty: false}); 24295 } 24296 }; 24297 24298 self.editorManager.on('BeforeUnload', self._beforeUnload); 24299 } 24300 24301 // Load scripts 24302 function loadScripts() { 24303 var scriptLoader = ScriptLoader.ScriptLoader; 24304 24305 if (settings.language && settings.language != 'en' && !settings.language_url) { 24306 settings.language_url = self.editorManager.baseURL + '/langs/' + settings.language + '.js'; 24307 } 24308 24309 if (settings.language_url) { 24310 scriptLoader.add(settings.language_url); 24311 } 24312 24313 if (settings.theme && typeof settings.theme != "function" && 24314 settings.theme.charAt(0) != '-' && !ThemeManager.urls[settings.theme]) { 24315 var themeUrl = settings.theme_url; 24316 24317 if (themeUrl) { 24318 themeUrl = self.documentBaseURI.toAbsolute(themeUrl); 24319 } else { 24320 themeUrl = 'themes/' + settings.theme + '/theme' + suffix + '.js'; 24321 } 24322 24323 ThemeManager.load(settings.theme, themeUrl); 24324 } 24325 24326 if (Tools.isArray(settings.plugins)) { 24327 settings.plugins = settings.plugins.join(' '); 24328 } 24329 24330 each(settings.external_plugins, function(url, name) { 24331 PluginManager.load(name, url); 24332 settings.plugins += ' ' + name; 24333 }); 24334 24335 each(settings.plugins.split(/[ ,]/), function(plugin) { 24336 plugin = trim(plugin); 24337 24338 if (plugin && !PluginManager.urls[plugin]) { 24339 if (plugin.charAt(0) == '-') { 24340 plugin = plugin.substr(1, plugin.length); 24341 24342 var dependencies = PluginManager.dependencies(plugin); 24343 24344 each(dependencies, function(dep) { 24345 var defaultSettings = { 24346 prefix:'plugins/', 24347 resource: dep, 24348 suffix:'/plugin' + suffix + '.js' 24349 }; 24350 24351 dep = PluginManager.createUrl(defaultSettings, dep); 24352 PluginManager.load(dep.resource, dep); 24353 }); 24354 } else { 24355 PluginManager.load(plugin, { 24356 prefix: 'plugins/', 24357 resource: plugin, 24358 suffix: '/plugin' + suffix + '.js' 24359 }); 24360 } 24361 } 24362 }); 24363 24364 scriptLoader.loadQueue(function() { 24365 if (!self.removed) { 24366 self.init(); 24367 } 24368 }); 24369 } 24370 24371 loadScripts(); 24372 }, 24373 24374 /** 24375 * Initializes the editor this will be called automatically when 24376 * all plugins/themes and language packs are loaded by the rendered method. 24377 * This method will setup the iframe and create the theme and plugin instances. 24378 * 24379 * @method init 24380 */ 24381 init: function() { 24382 var self = this, settings = self.settings, elm = self.getElement(); 24383 var w, h, minHeight, n, o, Theme, url, bodyId, bodyClass, re, i, initializedPlugins = []; 24384 24385 self.rtl = this.editorManager.i18n.rtl; 24386 self.editorManager.add(self); 24387 24388 settings.aria_label = settings.aria_label || DOM.getAttrib(elm, 'aria-label', self.getLang('aria.rich_text_area')); 24389 24390 /** 24391 * Reference to the theme instance that was used to generate the UI. 24392 * 24393 * @property theme 24394 * @type tinymce.Theme 24395 * @example 24396 * // Executes a method on the theme directly 24397 * tinymce.activeEditor.theme.someMethod(); 24398 */ 24399 if (settings.theme) { 24400 if (typeof settings.theme != "function") { 24401 settings.theme = settings.theme.replace(/-/, ''); 24402 Theme = ThemeManager.get(settings.theme); 24403 self.theme = new Theme(self, ThemeManager.urls[settings.theme]); 24404 24405 if (self.theme.init) { 24406 self.theme.init(self, ThemeManager.urls[settings.theme] || self.documentBaseUrl.replace(/\/$/, '')); 24407 } 24408 } else { 24409 self.theme = settings.theme; 24410 } 24411 } 24412 24413 function initPlugin(plugin) { 24414 var Plugin = PluginManager.get(plugin), pluginUrl, pluginInstance; 24415 24416 pluginUrl = PluginManager.urls[plugin] || self.documentBaseUrl.replace(/\/$/, ''); 24417 plugin = trim(plugin); 24418 if (Plugin && inArray(initializedPlugins, plugin) === -1) { 24419 each(PluginManager.dependencies(plugin), function(dep){ 24420 initPlugin(dep); 24421 }); 24422 24423 pluginInstance = new Plugin(self, pluginUrl); 24424 24425 self.plugins[plugin] = pluginInstance; 24426 24427 if (pluginInstance.init) { 24428 pluginInstance.init(self, pluginUrl); 24429 initializedPlugins.push(plugin); 24430 } 24431 } 24432 } 24433 24434 // Create all plugins 24435 each(settings.plugins.replace(/\-/g, '').split(/[ ,]/), initPlugin); 24436 24437 // Measure box 24438 if (settings.render_ui && self.theme) { 24439 self.orgDisplay = elm.style.display; 24440 24441 if (typeof settings.theme != "function") { 24442 w = settings.width || elm.style.width || elm.offsetWidth; 24443 h = settings.height || elm.style.height || elm.offsetHeight; 24444 minHeight = settings.min_height || 100; 24445 re = /^[0-9\.]+(|px)$/i; 24446 24447 if (re.test('' + w)) { 24448 w = Math.max(parseInt(w, 10), 100); 24449 } 24450 24451 if (re.test('' + h)) { 24452 h = Math.max(parseInt(h, 10), minHeight); 24453 } 24454 24455 // Render UI 24456 o = self.theme.renderUI({ 24457 targetNode: elm, 24458 width: w, 24459 height: h, 24460 deltaWidth: settings.delta_width, 24461 deltaHeight: settings.delta_height 24462 }); 24463 24464 // Resize editor 24465 if (!settings.content_editable) { 24466 DOM.setStyles(o.sizeContainer || o.editorContainer, { 24467 wi2dth: w, 24468 // TODO: Fix this 24469 h2eight: h 24470 }); 24471 24472 h = (o.iframeHeight || h) + (typeof(h) == 'number' ? (o.deltaHeight || 0) : ''); 24473 if (h < minHeight) { 24474 h = minHeight; 24475 } 24476 } 24477 } else { 24478 o = settings.theme(self, elm); 24479 24480 // Convert element type to id:s 24481 if (o.editorContainer.nodeType) { 24482 o.editorContainer = o.editorContainer.id = o.editorContainer.id || self.id + "_parent"; 24483 } 24484 24485 // Convert element type to id:s 24486 if (o.iframeContainer.nodeType) { 24487 o.iframeContainer = o.iframeContainer.id = o.iframeContainer.id || self.id + "_iframecontainer"; 24488 } 24489 24490 // Use specified iframe height or the targets offsetHeight 24491 h = o.iframeHeight || elm.offsetHeight; 24492 } 24493 24494 self.editorContainer = o.editorContainer; 24495 } 24496 24497 // Load specified content CSS last 24498 if (settings.content_css) { 24499 each(explode(settings.content_css), function(u) { 24500 self.contentCSS.push(self.documentBaseURI.toAbsolute(u)); 24501 }); 24502 } 24503 24504 // Load specified content CSS last 24505 if (settings.content_style) { 24506 self.contentStyles.push(settings.content_style); 24507 } 24508 24509 // Content editable mode ends here 24510 if (settings.content_editable) { 24511 elm = n = o = null; // Fix IE leak 24512 return self.initContentBody(); 24513 } 24514 24515 self.iframeHTML = settings.doctype + '<html><head>'; 24516 24517 // We only need to override paths if we have to 24518 // IE has a bug where it remove site absolute urls to relative ones if this is specified 24519 if (settings.document_base_url != self.documentBaseUrl) { 24520 self.iframeHTML += '<base href="' + self.documentBaseURI.getURI() + '" />'; 24521 } 24522 24523 // IE8 doesn't support carets behind images setting ie7_compat would force IE8+ to run in IE7 compat mode. 24524 if (!Env.caretAfter && settings.ie7_compat) { 24525 self.iframeHTML += '<meta http-equiv="X-UA-Compatible" content="IE=7" />'; 24526 } 24527 24528 self.iframeHTML += '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />'; 24529 24530 // Load the CSS by injecting them into the HTML this will reduce "flicker" 24531 for (i = 0; i < self.contentCSS.length; i++) { 24532 var cssUrl = self.contentCSS[i]; 24533 self.iframeHTML += '<link type="text/css" rel="stylesheet" href="' + cssUrl + '" />'; 24534 self.loadedCSS[cssUrl] = true; 24535 } 24536 24537 bodyId = settings.body_id || 'tinymce'; 24538 if (bodyId.indexOf('=') != -1) { 24539 bodyId = self.getParam('body_id', '', 'hash'); 24540 bodyId = bodyId[self.id] || bodyId; 24541 } 24542 24543 bodyClass = settings.body_class || ''; 24544 if (bodyClass.indexOf('=') != -1) { 24545 bodyClass = self.getParam('body_class', '', 'hash'); 24546 bodyClass = bodyClass[self.id] || ''; 24547 } 24548 24549 self.iframeHTML += '</head><body id="' + bodyId + '" class="mce-content-body ' + bodyClass + '" ' + 24550 'onload="window.parent.tinymce.get(\'' + self.id + '\').fire(\'load\');"><br></body></html>'; 24551 24552 /*eslint no-script-url:0 */ 24553 var domainRelaxUrl = 'javascript:(function(){' + 24554 'document.open();document.domain="' + document.domain + '";' + 24555 'var ed = window.parent.tinymce.get("' + self.id + '");document.write(ed.iframeHTML);' + 24556 'document.close();ed.initContentBody(true);})()'; 24557 24558 // Domain relaxing is required since the user has messed around with document.domain 24559 if (document.domain != location.hostname) { 24560 url = domainRelaxUrl; 24561 } 24562 24563 // Create iframe 24564 // TODO: ACC add the appropriate description on this. 24565 n = DOM.add(o.iframeContainer, 'iframe', { 24566 id: self.id + "_ifr", 24567 src: url || 'javascript:""', // Workaround for HTTPS warning in IE6/7 24568 frameBorder: '0', 24569 allowTransparency: "true", 24570 title: self.editorManager.translate( 24571 "Rich Text Area. Press ALT-F9 for menu. " + 24572 "Press ALT-F10 for toolbar. Press ALT-0 for help" 24573 ), 24574 style: { 24575 width: '100%', 24576 height: h, 24577 display: 'block' // Important for Gecko to render the iframe correctly 24578 } 24579 }); 24580 24581 // Try accessing the document this will fail on IE when document.domain is set to the same as location.hostname 24582 // Then we have to force domain relaxing using the domainRelaxUrl approach very ugly!! 24583 if (ie) { 24584 try { 24585 self.getDoc(); 24586 } catch (e) { 24587 n.src = url = domainRelaxUrl; 24588 } 24589 } 24590 24591 self.contentAreaContainer = o.iframeContainer; 24592 24593 if (o.editorContainer) { 24594 DOM.get(o.editorContainer).style.display = self.orgDisplay; 24595 } 24596 24597 DOM.get(self.id).style.display = 'none'; 24598 DOM.setAttrib(self.id, 'aria-hidden', true); 24599 24600 if (!url) { 24601 self.initContentBody(); 24602 } 24603 24604 elm = n = o = null; // Cleanup 24605 }, 24606 24607 /** 24608 * This method get called by the init method ones the iframe is loaded. 24609 * It will fill the iframe with contents, setups DOM and selection objects for the iframe. 24610 * 24611 * @method initContentBody 24612 * @private 24613 */ 24614 initContentBody: function(skipWrite) { 24615 var self = this, settings = self.settings, targetElm = DOM.get(self.id), doc = self.getDoc(), body, contentCssText; 24616 24617 // Restore visibility on target element 24618 if (!settings.inline) { 24619 self.getElement().style.visibility = self.orgVisibility; 24620 } 24621 24622 // Setup iframe body 24623 if (!skipWrite && !settings.content_editable) { 24624 doc.open(); 24625 doc.write(self.iframeHTML); 24626 doc.close(); 24627 } 24628 24629 if (settings.content_editable) { 24630 self.on('remove', function() { 24631 var bodyEl = this.getBody(); 24632 24633 DOM.removeClass(bodyEl, 'mce-content-body'); 24634 DOM.removeClass(bodyEl, 'mce-edit-focus'); 24635 DOM.setAttrib(bodyEl, 'contentEditable', null); 24636 }); 24637 24638 DOM.addClass(targetElm, 'mce-content-body'); 24639 self.contentDocument = doc = settings.content_document || document; 24640 self.contentWindow = settings.content_window || window; 24641 self.bodyElement = targetElm; 24642 24643 // Prevent leak in IE 24644 settings.content_document = settings.content_window = null; 24645 24646 // TODO: Fix this 24647 settings.root_name = targetElm.nodeName.toLowerCase(); 24648 } 24649 24650 // It will not steal focus while setting contentEditable 24651 body = self.getBody(); 24652 body.disabled = true; 24653 24654 if (!settings.readonly) { 24655 if (self.inline && DOM.getStyle(body, 'position', true) == 'static') { 24656 body.style.position = 'relative'; 24657 } 24658 24659 body.contentEditable = self.getParam('content_editable_state', true); 24660 } 24661 24662 body.disabled = false; 24663 24664 /** 24665 * Schema instance, enables you to validate elements and it's children. 24666 * 24667 * @property schema 24668 * @type tinymce.html.Schema 24669 */ 24670 self.schema = new Schema(settings); 24671 24672 /** 24673 * DOM instance for the editor. 24674 * 24675 * @property dom 24676 * @type tinymce.dom.DOMUtils 24677 * @example 24678 * // Adds a class to all paragraphs within the editor 24679 * tinymce.activeEditor.dom.addClass(tinymce.activeEditor.dom.select('p'), 'someclass'); 24680 */ 24681 self.dom = new DOMUtils(doc, { 24682 keep_values: true, 24683 url_converter: self.convertURL, 24684 url_converter_scope: self, 24685 hex_colors: settings.force_hex_style_colors, 24686 class_filter: settings.class_filter, 24687 update_styles: true, 24688 root_element: settings.content_editable ? self.id : null, 24689 collect: settings.content_editable, 24690 schema: self.schema, 24691 onSetAttrib: function(e) { 24692 self.fire('SetAttrib', e); 24693 } 24694 }); 24695 24696 /** 24697 * HTML parser will be used when contents is inserted into the editor. 24698 * 24699 * @property parser 24700 * @type tinymce.html.DomParser 24701 */ 24702 self.parser = new DomParser(settings, self.schema); 24703 24704 // Convert src and href into data-mce-src, data-mce-href and data-mce-style 24705 self.parser.addAttributeFilter('src,href,style,tabindex', function(nodes, name) { 24706 var i = nodes.length, node, dom = self.dom, value, internalName; 24707 24708 while (i--) { 24709 node = nodes[i]; 24710 value = node.attr(name); 24711 internalName = 'data-mce-' + name; 24712 24713 // Add internal attribute if we need to we don't on a refresh of the document 24714 if (!node.attributes.map[internalName]) { 24715 if (name === "style") { 24716 node.attr(internalName, dom.serializeStyle(dom.parseStyle(value), node.name)); 24717 } else if (name === "tabindex") { 24718 node.attr(internalName, value); 24719 node.attr(name, null); 24720 } else { 24721 node.attr(internalName, self.convertURL(value, name, node.name)); 24722 } 24723 } 24724 } 24725 }); 24726 24727 // Keep scripts from executing 24728 self.parser.addNodeFilter('script', function(nodes) { 24729 var i = nodes.length, node; 24730 24731 while (i--) { 24732 node = nodes[i]; 24733 node.attr('type', 'mce-' + (node.attr('type') || 'text/javascript')); 24734 } 24735 }); 24736 24737 self.parser.addNodeFilter('#cdata', function(nodes) { 24738 var i = nodes.length, node; 24739 24740 while (i--) { 24741 node = nodes[i]; 24742 node.type = 8; 24743 node.name = '#comment'; 24744 node.value = '[CDATA[' + node.value + ']]'; 24745 } 24746 }); 24747 24748 self.parser.addNodeFilter('p,h1,h2,h3,h4,h5,h6,div', function(nodes) { 24749 var i = nodes.length, node, nonEmptyElements = self.schema.getNonEmptyElements(); 24750 24751 while (i--) { 24752 node = nodes[i]; 24753 24754 if (node.isEmpty(nonEmptyElements)) { 24755 node.empty().append(new Node('br', 1)).shortEnded = true; 24756 } 24757 } 24758 }); 24759 24760 /** 24761 * DOM serializer for the editor. Will be used when contents is extracted from the editor. 24762 * 24763 * @property serializer 24764 * @type tinymce.dom.Serializer 24765 * @example 24766 * // Serializes the first paragraph in the editor into a string 24767 * tinymce.activeEditor.serializer.serialize(tinymce.activeEditor.dom.select('p')[0]); 24768 */ 24769 self.serializer = new DomSerializer(settings, self); 24770 24771 /** 24772 * Selection instance for the editor. 24773 * 24774 * @property selection 24775 * @type tinymce.dom.Selection 24776 * @example 24777 * // Sets some contents to the current selection in the editor 24778 * tinymce.activeEditor.selection.setContent('Some contents'); 24779 * 24780 * // Gets the current selection 24781 * alert(tinymce.activeEditor.selection.getContent()); 24782 * 24783 * // Selects the first paragraph found 24784 * tinymce.activeEditor.selection.select(tinymce.activeEditor.dom.select('p')[0]); 24785 */ 24786 self.selection = new Selection(self.dom, self.getWin(), self.serializer, self); 24787 24788 /** 24789 * Formatter instance. 24790 * 24791 * @property formatter 24792 * @type tinymce.Formatter 24793 */ 24794 self.formatter = new Formatter(self); 24795 24796 /** 24797 * Undo manager instance, responsible for handling undo levels. 24798 * 24799 * @property undoManager 24800 * @type tinymce.UndoManager 24801 * @example 24802 * // Undoes the last modification to the editor 24803 * tinymce.activeEditor.undoManager.undo(); 24804 */ 24805 self.undoManager = new UndoManager(self); 24806 24807 self.forceBlocks = new ForceBlocks(self); 24808 self.enterKey = new EnterKey(self); 24809 self.editorCommands = new EditorCommands(self); 24810 24811 self.fire('PreInit'); 24812 24813 if (!settings.browser_spellcheck && !settings.gecko_spellcheck) { 24814 doc.body.spellcheck = false; // Gecko 24815 DOM.setAttrib(body, "spellcheck", "false"); 24816 } 24817 24818 self.fire('PostRender'); 24819 24820 self.quirks = Quirks(self); 24821 24822 if (settings.directionality) { 24823 body.dir = settings.directionality; 24824 } 24825 24826 if (settings.nowrap) { 24827 body.style.whiteSpace = "nowrap"; 24828 } 24829 24830 if (settings.protect) { 24831 self.on('BeforeSetContent', function(e) { 24832 each(settings.protect, function(pattern) { 24833 e.content = e.content.replace(pattern, function(str) { 24834 return '<!--mce:protected ' + escape(str) + '-->'; 24835 }); 24836 }); 24837 }); 24838 } 24839 24840 self.on('SetContent', function() { 24841 self.addVisual(self.getBody()); 24842 }); 24843 24844 // Remove empty contents 24845 if (settings.padd_empty_editor) { 24846 self.on('PostProcess', function(e) { 24847 e.content = e.content.replace(/^(<p[^>]*>( | |\s|\u00a0|)<\/p>[\r\n]*|<br \/>[\r\n]*)$/, ''); 24848 }); 24849 } 24850 24851 self.load({initial: true, format: 'html'}); 24852 self.startContent = self.getContent({format: 'raw'}); 24853 24854 /** 24855 * Is set to true after the editor instance has been initialized 24856 * 24857 * @property initialized 24858 * @type Boolean 24859 * @example 24860 * function isEditorInitialized(editor) { 24861 * return editor && editor.initialized; 24862 * } 24863 */ 24864 self.initialized = true; 24865 self.bindPendingEventDelegates(); 24866 24867 self.fire('init'); 24868 self.focus(true); 24869 self.nodeChanged({initial: true}); 24870 self.execCallback('init_instance_callback', self); 24871 24872 // Add editor specific CSS styles 24873 if (self.contentStyles.length > 0) { 24874 contentCssText = ''; 24875 24876 each(self.contentStyles, function(style) { 24877 contentCssText += style + "\r\n"; 24878 }); 24879 24880 self.dom.addStyle(contentCssText); 24881 } 24882 24883 // Load specified content CSS last 24884 each(self.contentCSS, function(cssUrl) { 24885 if (!self.loadedCSS[cssUrl]) { 24886 self.dom.loadCSS(cssUrl); 24887 self.loadedCSS[cssUrl] = true; 24888 } 24889 }); 24890 24891 // Handle auto focus 24892 if (settings.auto_focus) { 24893 setTimeout(function () { 24894 var ed = self.editorManager.get(settings.auto_focus); 24895 24896 ed.selection.select(ed.getBody(), 1); 24897 ed.selection.collapse(1); 24898 ed.getBody().focus(); 24899 ed.getWin().focus(); 24900 }, 100); 24901 } 24902 24903 // Clean up references for IE 24904 targetElm = doc = body = null; 24905 }, 24906 24907 /** 24908 * Focuses/activates the editor. This will set this editor as the activeEditor in the tinymce collection 24909 * it will also place DOM focus inside the editor. 24910 * 24911 * @method focus 24912 * @param {Boolean} skip_focus Skip DOM focus. Just set is as the active editor. 24913 */ 24914 focus: function(skip_focus) { 24915 var oed, self = this, selection = self.selection, contentEditable = self.settings.content_editable, rng; 24916 var controlElm, doc = self.getDoc(), body; 24917 24918 if (!skip_focus) { 24919 // Get selected control element 24920 rng = selection.getRng(); 24921 if (rng.item) { 24922 controlElm = rng.item(0); 24923 } 24924 24925 self._refreshContentEditable(); 24926 24927 // Focus the window iframe 24928 if (!contentEditable) { 24929 // WebKit needs this call to fire focusin event properly see #5948 24930 // But Opera pre Blink engine will produce an empty selection so skip Opera 24931 if (!Env.opera) { 24932 self.getBody().focus(); 24933 } 24934 24935 self.getWin().focus(); 24936 } 24937 24938 // Focus the body as well since it's contentEditable 24939 if (isGecko || contentEditable) { 24940 body = self.getBody(); 24941 24942 // Check for setActive since it doesn't scroll to the element 24943 if (body.setActive) { 24944 // IE 11 sometimes throws "Invalid function" then fallback to focus 24945 try { 24946 body.setActive(); 24947 } catch (ex) { 24948 body.focus(); 24949 } 24950 } else { 24951 body.focus(); 24952 } 24953 24954 if (contentEditable) { 24955 selection.normalize(); 24956 } 24957 } 24958 24959 // Restore selected control element 24960 // This is needed when for example an image is selected within a 24961 // layer a call to focus will then remove the control selection 24962 if (controlElm && controlElm.ownerDocument == doc) { 24963 rng = doc.body.createControlRange(); 24964 rng.addElement(controlElm); 24965 rng.select(); 24966 } 24967 } 24968 24969 if (self.editorManager.activeEditor != self) { 24970 if ((oed = self.editorManager.activeEditor)) { 24971 oed.fire('deactivate', {relatedTarget: self}); 24972 } 24973 24974 self.fire('activate', {relatedTarget: oed}); 24975 } 24976 24977 self.editorManager.activeEditor = self; 24978 }, 24979 24980 /** 24981 * Executes a legacy callback. This method is useful to call old 2.x option callbacks. 24982 * There new event model is a better way to add callback so this method might be removed in the future. 24983 * 24984 * @method execCallback 24985 * @param {String} name Name of the callback to execute. 24986 * @return {Object} Return value passed from callback function. 24987 */ 24988 execCallback: function(name) { 24989 var self = this, callback = self.settings[name], scope; 24990 24991 if (!callback) { 24992 return; 24993 } 24994 24995 // Look through lookup 24996 if (self.callbackLookup && (scope = self.callbackLookup[name])) { 24997 callback = scope.func; 24998 scope = scope.scope; 24999 } 25000 25001 if (typeof(callback) === 'string') { 25002 scope = callback.replace(/\.\w+$/, ''); 25003 scope = scope ? resolve(scope) : 0; 25004 callback = resolve(callback); 25005 self.callbackLookup = self.callbackLookup || {}; 25006 self.callbackLookup[name] = {func: callback, scope: scope}; 25007 } 25008 25009 return callback.apply(scope || self, Array.prototype.slice.call(arguments, 1)); 25010 }, 25011 25012 /** 25013 * Translates the specified string by replacing variables with language pack items it will also check if there is 25014 * a key mathcin the input. 25015 * 25016 * @method translate 25017 * @param {String} text String to translate by the language pack data. 25018 * @return {String} Translated string. 25019 */ 25020 translate: function(text) { 25021 var lang = this.settings.language || 'en', i18n = this.editorManager.i18n; 25022 25023 if (!text) { 25024 return ''; 25025 } 25026 25027 return i18n.data[lang + '.' + text] || text.replace(/\{\#([^\}]+)\}/g, function(a, b) { 25028 return i18n.data[lang + '.' + b] || '{#' + b + '}'; 25029 }); 25030 }, 25031 25032 /** 25033 * Returns a language pack item by name/key. 25034 * 25035 * @method getLang 25036 * @param {String} name Name/key to get from the language pack. 25037 * @param {String} defaultVal Optional default value to retrive. 25038 */ 25039 getLang: function(name, defaultVal) { 25040 return ( 25041 this.editorManager.i18n.data[(this.settings.language || 'en') + '.' + name] || 25042 (defaultVal !== undefined ? defaultVal : '{#' + name + '}') 25043 ); 25044 }, 25045 25046 /** 25047 * Returns a configuration parameter by name. 25048 * 25049 * @method getParam 25050 * @param {String} name Configruation parameter to retrive. 25051 * @param {String} defaultVal Optional default value to return. 25052 * @param {String} type Optional type parameter. 25053 * @return {String} Configuration parameter value or default value. 25054 * @example 25055 * // Returns a specific config value from the currently active editor 25056 * var someval = tinymce.activeEditor.getParam('myvalue'); 25057 * 25058 * // Returns a specific config value from a specific editor instance by id 25059 * var someval2 = tinymce.get('my_editor').getParam('myvalue'); 25060 */ 25061 getParam: function(name, defaultVal, type) { 25062 var value = name in this.settings ? this.settings[name] : defaultVal, output; 25063 25064 if (type === 'hash') { 25065 output = {}; 25066 25067 if (typeof(value) === 'string') { 25068 each(value.indexOf('=') > 0 ? value.split(/[;,](?![^=;,]*(?:[;,]|$))/) : value.split(','), function(value) { 25069 value = value.split('='); 25070 25071 if (value.length > 1) { 25072 output[trim(value[0])] = trim(value[1]); 25073 } else { 25074 output[trim(value[0])] = trim(value); 25075 } 25076 }); 25077 } else { 25078 output = value; 25079 } 25080 25081 return output; 25082 } 25083 25084 return value; 25085 }, 25086 25087 /** 25088 * Distpaches out a onNodeChange event to all observers. This method should be called when you 25089 * need to update the UI states or element path etc. 25090 * 25091 * @method nodeChanged 25092 */ 25093 nodeChanged: function() { 25094 var self = this, selection = self.selection, node, parents, root; 25095 25096 // Fix for bug #1896577 it seems that this can not be fired while the editor is loading 25097 if (self.initialized && !self.settings.disable_nodechange && !self.settings.readonly) { 25098 // Get start node 25099 root = self.getBody(); 25100 node = selection.getStart() || root; 25101 node = ie && node.ownerDocument != self.getDoc() ? self.getBody() : node; // Fix for IE initial state 25102 25103 // Edge case for <p>|<img></p> 25104 if (node.nodeName == 'IMG' && selection.isCollapsed()) { 25105 node = node.parentNode; 25106 } 25107 25108 // Get parents and add them to object 25109 parents = []; 25110 self.dom.getParent(node, function(node) { 25111 if (node === root) { 25112 return true; 25113 } 25114 25115 parents.push(node); 25116 }); 25117 25118 self.fire('NodeChange', {element: node, parents: parents}); 25119 } 25120 }, 25121 25122 /** 25123 * Adds a button that later gets created by the theme in the editors toolbars. 25124 * 25125 * @method addButton 25126 * @param {String} name Button name to add. 25127 * @param {Object} settings Settings object with title, cmd etc. 25128 * @example 25129 * // Adds a custom button to the editor that inserts contents when clicked 25130 * tinymce.init({ 25131 * ... 25132 * 25133 * toolbar: 'example' 25134 * 25135 * setup: function(ed) { 25136 * ed.addButton('example', { 25137 * title: 'My title', 25138 * image: '../js/tinymce/plugins/example/img/example.gif', 25139 * onclick: function() { 25140 * ed.insertContent('Hello world!!'); 25141 * } 25142 * }); 25143 * } 25144 * }); 25145 */ 25146 addButton: function(name, settings) { 25147 var self = this; 25148 25149 if (settings.cmd) { 25150 settings.onclick = function() { 25151 self.execCommand(settings.cmd); 25152 }; 25153 } 25154 25155 if (!settings.text && !settings.icon) { 25156 settings.icon = name; 25157 } 25158 25159 self.buttons = self.buttons || {}; 25160 settings.tooltip = settings.tooltip || settings.title; 25161 self.buttons[name] = settings; 25162 }, 25163 25164 /** 25165 * Adds a menu item to be used in the menus of the theme. There might be multiple instances 25166 * of this menu item for example it might be used in the main menus of the theme but also in 25167 * the context menu so make sure that it's self contained and supports multiple instances. 25168 * 25169 * @method addMenuItem 25170 * @param {String} name Menu item name to add. 25171 * @param {Object} settings Settings object with title, cmd etc. 25172 * @example 25173 * // Adds a custom menu item to the editor that inserts contents when clicked 25174 * // The context option allows you to add the menu item to an existing default menu 25175 * tinymce.init({ 25176 * ... 25177 * 25178 * setup: function(ed) { 25179 * ed.addMenuItem('example', { 25180 * text: 'My menu item', 25181 * context: 'tools', 25182 * onclick: function() { 25183 * ed.insertContent('Hello world!!'); 25184 * } 25185 * }); 25186 * } 25187 * }); 25188 */ 25189 addMenuItem: function(name, settings) { 25190 var self = this; 25191 25192 if (settings.cmd) { 25193 settings.onclick = function() { 25194 self.execCommand(settings.cmd); 25195 }; 25196 } 25197 25198 self.menuItems = self.menuItems || {}; 25199 self.menuItems[name] = settings; 25200 }, 25201 25202 /** 25203 * Adds a custom command to the editor, you can also override existing commands with this method. 25204 * The command that you add can be executed with execCommand. 25205 * 25206 * @method addCommand 25207 * @param {String} name Command name to add/override. 25208 * @param {addCommandCallback} callback Function to execute when the command occurs. 25209 * @param {Object} scope Optional scope to execute the function in. 25210 * @example 25211 * // Adds a custom command that later can be executed using execCommand 25212 * tinymce.init({ 25213 * ... 25214 * 25215 * setup: function(ed) { 25216 * // Register example command 25217 * ed.addCommand('mycommand', function(ui, v) { 25218 * ed.windowManager.alert('Hello world!! Selection: ' + ed.selection.getContent({format: 'text'})); 25219 * }); 25220 * } 25221 * }); 25222 */ 25223 addCommand: function(name, callback, scope) { 25224 /** 25225 * Callback function that gets called when a command is executed. 25226 * 25227 * @callback addCommandCallback 25228 * @param {Boolean} ui Display UI state true/false. 25229 * @param {Object} value Optional value for command. 25230 * @return {Boolean} True/false state if the command was handled or not. 25231 */ 25232 this.execCommands[name] = {func: callback, scope: scope || this}; 25233 }, 25234 25235 /** 25236 * Adds a custom query state command to the editor, you can also override existing commands with this method. 25237 * The command that you add can be executed with queryCommandState function. 25238 * 25239 * @method addQueryStateHandler 25240 * @param {String} name Command name to add/override. 25241 * @param {addQueryStateHandlerCallback} callback Function to execute when the command state retrival occurs. 25242 * @param {Object} scope Optional scope to execute the function in. 25243 */ 25244 addQueryStateHandler: function(name, callback, scope) { 25245 /** 25246 * Callback function that gets called when a queryCommandState is executed. 25247 * 25248 * @callback addQueryStateHandlerCallback 25249 * @return {Boolean} True/false state if the command is enabled or not like is it bold. 25250 */ 25251 this.queryStateCommands[name] = {func: callback, scope: scope || this}; 25252 }, 25253 25254 /** 25255 * Adds a custom query value command to the editor, you can also override existing commands with this method. 25256 * The command that you add can be executed with queryCommandValue function. 25257 * 25258 * @method addQueryValueHandler 25259 * @param {String} name Command name to add/override. 25260 * @param {addQueryValueHandlerCallback} callback Function to execute when the command value retrival occurs. 25261 * @param {Object} scope Optional scope to execute the function in. 25262 */ 25263 addQueryValueHandler: function(name, callback, scope) { 25264 /** 25265 * Callback function that gets called when a queryCommandValue is executed. 25266 * 25267 * @callback addQueryValueHandlerCallback 25268 * @return {Object} Value of the command or undefined. 25269 */ 25270 this.queryValueCommands[name] = {func: callback, scope: scope || this}; 25271 }, 25272 25273 /** 25274 * Adds a keyboard shortcut for some command or function. 25275 * 25276 * @method addShortcut 25277 * @param {String} pattern Shortcut pattern. Like for example: ctrl+alt+o. 25278 * @param {String} desc Text description for the command. 25279 * @param {String/Function} cmdFunc Command name string or function to execute when the key is pressed. 25280 * @param {Object} sc Optional scope to execute the function in. 25281 * @return {Boolean} true/false state if the shortcut was added or not. 25282 */ 25283 addShortcut: function(pattern, desc, cmdFunc, scope) { 25284 this.shortcuts.add(pattern, desc, cmdFunc, scope); 25285 }, 25286 25287 /** 25288 * Executes a command on the current instance. These commands can be TinyMCE internal commands prefixed with "mce" or 25289 * they can be build in browser commands such as "Bold". A compleate list of browser commands is available on MSDN or Mozilla.org. 25290 * This function will dispatch the execCommand function on each plugin, theme or the execcommand_callback option if none of these 25291 * return true it will handle the command as a internal browser command. 25292 * 25293 * @method execCommand 25294 * @param {String} cmd Command name to execute, for example mceLink or Bold. 25295 * @param {Boolean} ui True/false state if a UI (dialog) should be presented or not. 25296 * @param {mixed} value Optional command value, this can be anything. 25297 * @param {Object} a Optional arguments object. 25298 */ 25299 execCommand: function(cmd, ui, value, args) { 25300 var self = this, state = 0, cmdItem; 25301 25302 if (!/^(mceAddUndoLevel|mceEndUndoLevel|mceBeginUndoLevel|mceRepaint)$/.test(cmd) && (!args || !args.skip_focus)) { 25303 self.focus(); 25304 } 25305 25306 args = extend({}, args); 25307 args = self.fire('BeforeExecCommand', {command: cmd, ui: ui, value: value}); 25308 if (args.isDefaultPrevented()) { 25309 return false; 25310 } 25311 25312 // Registred commands 25313 if ((cmdItem = self.execCommands[cmd])) { 25314 // Fall through on true 25315 if (cmdItem.func.call(cmdItem.scope, ui, value) !== true) { 25316 self.fire('ExecCommand', {command: cmd, ui: ui, value: value}); 25317 return true; 25318 } 25319 } 25320 25321 // Plugin commands 25322 each(self.plugins, function(p) { 25323 if (p.execCommand && p.execCommand(cmd, ui, value)) { 25324 self.fire('ExecCommand', {command: cmd, ui: ui, value: value}); 25325 state = true; 25326 return false; 25327 } 25328 }); 25329 25330 if (state) { 25331 return state; 25332 } 25333 25334 // Theme commands 25335 if (self.theme && self.theme.execCommand && self.theme.execCommand(cmd, ui, value)) { 25336 self.fire('ExecCommand', {command: cmd, ui: ui, value: value}); 25337 return true; 25338 } 25339 25340 // Editor commands 25341 if (self.editorCommands.execCommand(cmd, ui, value)) { 25342 self.fire('ExecCommand', {command: cmd, ui: ui, value: value}); 25343 return true; 25344 } 25345 25346 // Browser commands 25347 self.getDoc().execCommand(cmd, ui, value); 25348 self.fire('ExecCommand', {command: cmd, ui: ui, value: value}); 25349 }, 25350 25351 /** 25352 * Returns a command specific state, for example if bold is enabled or not. 25353 * 25354 * @method queryCommandState 25355 * @param {string} cmd Command to query state from. 25356 * @return {Boolean} Command specific state, for example if bold is enabled or not. 25357 */ 25358 queryCommandState: function(cmd) { 25359 var self = this, queryItem, returnVal; 25360 25361 // Is hidden then return undefined 25362 if (self._isHidden()) { 25363 return; 25364 } 25365 25366 // Registred commands 25367 if ((queryItem = self.queryStateCommands[cmd])) { 25368 returnVal = queryItem.func.call(queryItem.scope); 25369 25370 // Fall though on true 25371 if (returnVal !== true) { 25372 return returnVal; 25373 } 25374 } 25375 25376 // Editor commands 25377 returnVal = self.editorCommands.queryCommandState(cmd); 25378 if (returnVal !== -1) { 25379 return returnVal; 25380 } 25381 25382 // Browser commands 25383 try { 25384 return self.getDoc().queryCommandState(cmd); 25385 } catch (ex) { 25386 // Fails sometimes see bug: 1896577 25387 } 25388 }, 25389 25390 /** 25391 * Returns a command specific value, for example the current font size. 25392 * 25393 * @method queryCommandValue 25394 * @param {string} cmd Command to query value from. 25395 * @return {Object} Command specific value, for example the current font size. 25396 */ 25397 queryCommandValue: function(cmd) { 25398 var self = this, queryItem, returnVal; 25399 25400 // Is hidden then return undefined 25401 if (self._isHidden()) { 25402 return; 25403 } 25404 25405 // Registred commands 25406 if ((queryItem = self.queryValueCommands[cmd])) { 25407 returnVal = queryItem.func.call(queryItem.scope); 25408 25409 // Fall though on true 25410 if (returnVal !== true) { 25411 return returnVal; 25412 } 25413 } 25414 25415 // Editor commands 25416 returnVal = self.editorCommands.queryCommandValue(cmd); 25417 if (returnVal !== undefined) { 25418 return returnVal; 25419 } 25420 25421 // Browser commands 25422 try { 25423 return self.getDoc().queryCommandValue(cmd); 25424 } catch (ex) { 25425 // Fails sometimes see bug: 1896577 25426 } 25427 }, 25428 25429 /** 25430 * Shows the editor and hides any textarea/div that the editor is supposed to replace. 25431 * 25432 * @method show 25433 */ 25434 show: function() { 25435 var self = this; 25436 25437 if (self.hidden) { 25438 self.hidden = false; 25439 25440 if (self.inline) { 25441 self.getBody().contentEditable = true; 25442 } else { 25443 DOM.show(self.getContainer()); 25444 DOM.hide(self.id); 25445 } 25446 25447 self.load(); 25448 self.fire('show'); 25449 } 25450 }, 25451 25452 /** 25453 * Hides the editor and shows any textarea/div that the editor is supposed to replace. 25454 * 25455 * @method hide 25456 */ 25457 hide: function() { 25458 var self = this, doc = self.getDoc(); 25459 25460 if (!self.hidden) { 25461 self.hidden = true; 25462 25463 // Fixed bug where IE has a blinking cursor left from the editor 25464 if (ie && doc && !self.inline) { 25465 doc.execCommand('SelectAll'); 25466 } 25467 25468 // We must save before we hide so Safari doesn't crash 25469 self.save(); 25470 25471 if (self.inline) { 25472 self.getBody().contentEditable = false; 25473 25474 // Make sure the editor gets blurred 25475 if (self == self.editorManager.focusedEditor) { 25476 self.editorManager.focusedEditor = null; 25477 } 25478 } else { 25479 DOM.hide(self.getContainer()); 25480 DOM.setStyle(self.id, 'display', self.orgDisplay); 25481 } 25482 25483 self.fire('hide'); 25484 } 25485 }, 25486 25487 /** 25488 * Returns true/false if the editor is hidden or not. 25489 * 25490 * @method isHidden 25491 * @return {Boolean} True/false if the editor is hidden or not. 25492 */ 25493 isHidden: function() { 25494 return !!this.hidden; 25495 }, 25496 25497 /** 25498 * Sets the progress state, this will display a throbber/progess for the editor. 25499 * This is ideal for asycronous operations like an AJAX save call. 25500 * 25501 * @method setProgressState 25502 * @param {Boolean} state Boolean state if the progress should be shown or hidden. 25503 * @param {Number} time Optional time to wait before the progress gets shown. 25504 * @return {Boolean} Same as the input state. 25505 * @example 25506 * // Show progress for the active editor 25507 * tinymce.activeEditor.setProgressState(true); 25508 * 25509 * // Hide progress for the active editor 25510 * tinymce.activeEditor.setProgressState(false); 25511 * 25512 * // Show progress after 3 seconds 25513 * tinymce.activeEditor.setProgressState(true, 3000); 25514 */ 25515 setProgressState: function(state, time) { 25516 this.fire('ProgressState', {state: state, time: time}); 25517 }, 25518 25519 /** 25520 * Loads contents from the textarea or div element that got converted into an editor instance. 25521 * This method will move the contents from that textarea or div into the editor by using setContent 25522 * so all events etc that method has will get dispatched as well. 25523 * 25524 * @method load 25525 * @param {Object} args Optional content object, this gets passed around through the whole load process. 25526 * @return {String} HTML string that got set into the editor. 25527 */ 25528 load: function(args) { 25529 var self = this, elm = self.getElement(), html; 25530 25531 if (elm) { 25532 args = args || {}; 25533 args.load = true; 25534 25535 html = self.setContent(elm.value !== undefined ? elm.value : elm.innerHTML, args); 25536 args.element = elm; 25537 25538 if (!args.no_events) { 25539 self.fire('LoadContent', args); 25540 } 25541 25542 args.element = elm = null; 25543 25544 return html; 25545 } 25546 }, 25547 25548 /** 25549 * Saves the contents from a editor out to the textarea or div element that got converted into an editor instance. 25550 * This method will move the HTML contents from the editor into that textarea or div by getContent 25551 * so all events etc that method has will get dispatched as well. 25552 * 25553 * @method save 25554 * @param {Object} args Optional content object, this gets passed around through the whole save process. 25555 * @return {String} HTML string that got set into the textarea/div. 25556 */ 25557 save: function(args) { 25558 var self = this, elm = self.getElement(), html, form; 25559 25560 if (!elm || !self.initialized) { 25561 return; 25562 } 25563 25564 args = args || {}; 25565 args.save = true; 25566 25567 args.element = elm; 25568 html = args.content = self.getContent(args); 25569 25570 if (!args.no_events) { 25571 self.fire('SaveContent', args); 25572 } 25573 25574 html = args.content; 25575 25576 if (!/TEXTAREA|INPUT/i.test(elm.nodeName)) { 25577 // Update DIV element when not in inline mode 25578 if (!self.inline) { 25579 elm.innerHTML = html; 25580 } 25581 25582 // Update hidden form element 25583 if ((form = DOM.getParent(self.id, 'form'))) { 25584 each(form.elements, function(elm) { 25585 if (elm.name == self.id) { 25586 elm.value = html; 25587 return false; 25588 } 25589 }); 25590 } 25591 } else { 25592 elm.value = html; 25593 } 25594 25595 args.element = elm = null; 25596 25597 if (args.set_dirty !== false) { 25598 self.isNotDirty = true; 25599 } 25600 25601 return html; 25602 }, 25603 25604 /** 25605 * Sets the specified content to the editor instance, this will cleanup the content before it gets set using 25606 * the different cleanup rules options. 25607 * 25608 * @method setContent 25609 * @param {String} content Content to set to editor, normally HTML contents but can be other formats as well. 25610 * @param {Object} args Optional content object, this gets passed around through the whole set process. 25611 * @return {String} HTML string that got set into the editor. 25612 * @example 25613 * // Sets the HTML contents of the activeEditor editor 25614 * tinymce.activeEditor.setContent('<span>some</span> html'); 25615 * 25616 * // Sets the raw contents of the activeEditor editor 25617 * tinymce.activeEditor.setContent('<span>some</span> html', {format: 'raw'}); 25618 * 25619 * // Sets the content of a specific editor (my_editor in this example) 25620 * tinymce.get('my_editor').setContent(data); 25621 * 25622 * // Sets the bbcode contents of the activeEditor editor if the bbcode plugin was added 25623 * tinymce.activeEditor.setContent('[b]some[/b] html', {format: 'bbcode'}); 25624 */ 25625 setContent: function(content, args) { 25626 var self = this, body = self.getBody(), forcedRootBlockName; 25627 25628 // Setup args object 25629 args = args || {}; 25630 args.format = args.format || 'html'; 25631 args.set = true; 25632 args.content = content; 25633 25634 // Do preprocessing 25635 if (!args.no_events) { 25636 self.fire('BeforeSetContent', args); 25637 } 25638 25639 content = args.content; 25640 25641 // Padd empty content in Gecko and Safari. Commands will otherwise fail on the content 25642 // It will also be impossible to place the caret in the editor unless there is a BR element present 25643 if (content.length === 0 || /^\s+$/.test(content)) { 25644 forcedRootBlockName = self.settings.forced_root_block; 25645 25646 // Check if forcedRootBlock is configured and that the block is a valid child of the body 25647 if (forcedRootBlockName && self.schema.isValidChild(body.nodeName.toLowerCase(), forcedRootBlockName.toLowerCase())) { 25648 // Padd with bogus BR elements on modern browsers and IE 7 and 8 since they don't render empty P tags properly 25649 content = ie && ie < 11 ? '' : '<br data-mce-bogus="1">'; 25650 content = self.dom.createHTML(forcedRootBlockName, self.settings.forced_root_block_attrs, content); 25651 } else if (!ie) { 25652 // We need to add a BR when forced_root_block is disabled on non IE browsers to place the caret 25653 content = '<br data-mce-bogus="1">'; 25654 } 25655 25656 body.innerHTML = content; 25657 25658 self.fire('SetContent', args); 25659 } else { 25660 // Parse and serialize the html 25661 if (args.format !== 'raw') { 25662 content = new Serializer({}, self.schema).serialize( 25663 self.parser.parse(content, {isRootContent: true}) 25664 ); 25665 } 25666 25667 // Set the new cleaned contents to the editor 25668 args.content = trim(content); 25669 self.dom.setHTML(body, args.content); 25670 25671 // Do post processing 25672 if (!args.no_events) { 25673 self.fire('SetContent', args); 25674 } 25675 25676 // Don't normalize selection if the focused element isn't the body in 25677 // content editable mode since it will steal focus otherwise 25678 /*if (!self.settings.content_editable || document.activeElement === self.getBody()) { 25679 self.selection.normalize(); 25680 }*/ 25681 } 25682 25683 return args.content; 25684 }, 25685 25686 /** 25687 * Gets the content from the editor instance, this will cleanup the content before it gets returned using 25688 * the different cleanup rules options. 25689 * 25690 * @method getContent 25691 * @param {Object} args Optional content object, this gets passed around through the whole get process. 25692 * @return {String} Cleaned content string, normally HTML contents. 25693 * @example 25694 * // Get the HTML contents of the currently active editor 25695 * console.debug(tinymce.activeEditor.getContent()); 25696 * 25697 * // Get the raw contents of the currently active editor 25698 * tinymce.activeEditor.getContent({format: 'raw'}); 25699 * 25700 * // Get content of a specific editor: 25701 * tinymce.get('content id').getContent() 25702 */ 25703 getContent: function(args) { 25704 var self = this, content, body = self.getBody(); 25705 25706 // Setup args object 25707 args = args || {}; 25708 args.format = args.format || 'html'; 25709 args.get = true; 25710 args.getInner = true; 25711 25712 // Do preprocessing 25713 if (!args.no_events) { 25714 self.fire('BeforeGetContent', args); 25715 } 25716 25717 // Get raw contents or by default the cleaned contents 25718 if (args.format == 'raw') { 25719 content = body.innerHTML; 25720 } else if (args.format == 'text') { 25721 content = body.innerText || body.textContent; 25722 } else { 25723 content = self.serializer.serialize(body, args); 25724 } 25725 25726 // Trim whitespace in beginning/end of HTML 25727 if (args.format != 'text') { 25728 args.content = trim(content); 25729 } else { 25730 args.content = content; 25731 } 25732 25733 // Do post processing 25734 if (!args.no_events) { 25735 self.fire('GetContent', args); 25736 } 25737 25738 return args.content; 25739 }, 25740 25741 /** 25742 * Inserts content at caret position. 25743 * 25744 * @method insertContent 25745 * @param {String} content Content to insert. 25746 */ 25747 insertContent: function(content) { 25748 this.execCommand('mceInsertContent', false, content); 25749 }, 25750 25751 /** 25752 * Returns true/false if the editor is dirty or not. It will get dirty if the user has made modifications to the contents. 25753 * 25754 * @method isDirty 25755 * @return {Boolean} True/false if the editor is dirty or not. It will get dirty if the user has made modifications to the contents. 25756 * @example 25757 * if (tinymce.activeEditor.isDirty()) 25758 * alert("You must save your contents."); 25759 */ 25760 isDirty: function() { 25761 return !this.isNotDirty; 25762 }, 25763 25764 /** 25765 * Returns the editors container element. The container element wrappes in 25766 * all the elements added to the page for the editor. Such as UI, iframe etc. 25767 * 25768 * @method getContainer 25769 * @return {Element} HTML DOM element for the editor container. 25770 */ 25771 getContainer: function() { 25772 var self = this; 25773 25774 if (!self.container) { 25775 self.container = DOM.get(self.editorContainer || self.id + '_parent'); 25776 } 25777 25778 return self.container; 25779 }, 25780 25781 /** 25782 * Returns the editors content area container element. The this element is the one who 25783 * holds the iframe or the editable element. 25784 * 25785 * @method getContentAreaContainer 25786 * @return {Element} HTML DOM element for the editor area container. 25787 */ 25788 getContentAreaContainer: function() { 25789 return this.contentAreaContainer; 25790 }, 25791 25792 /** 25793 * Returns the target element/textarea that got replaced with a TinyMCE editor instance. 25794 * 25795 * @method getElement 25796 * @return {Element} HTML DOM element for the replaced element. 25797 */ 25798 getElement: function() { 25799 return DOM.get(this.settings.content_element || this.id); 25800 }, 25801 25802 /** 25803 * Returns the iframes window object. 25804 * 25805 * @method getWin 25806 * @return {Window} Iframe DOM window object. 25807 */ 25808 getWin: function() { 25809 var self = this, elm; 25810 25811 if (!self.contentWindow) { 25812 elm = DOM.get(self.id + "_ifr"); 25813 25814 if (elm) { 25815 self.contentWindow = elm.contentWindow; 25816 } 25817 } 25818 25819 return self.contentWindow; 25820 }, 25821 25822 /** 25823 * Returns the iframes document object. 25824 * 25825 * @method getDoc 25826 * @return {Document} Iframe DOM document object. 25827 */ 25828 getDoc: function() { 25829 var self = this, win; 25830 25831 if (!self.contentDocument) { 25832 win = self.getWin(); 25833 25834 if (win) { 25835 self.contentDocument = win.document; 25836 } 25837 } 25838 25839 return self.contentDocument; 25840 }, 25841 25842 /** 25843 * Returns the iframes body element. 25844 * 25845 * @method getBody 25846 * @return {Element} Iframe body element. 25847 */ 25848 getBody: function() { 25849 return this.bodyElement || this.getDoc().body; 25850 }, 25851 25852 /** 25853 * URL converter function this gets executed each time a user adds an img, a or 25854 * any other element that has a URL in it. This will be called both by the DOM and HTML 25855 * manipulation functions. 25856 * 25857 * @method convertURL 25858 * @param {string} url URL to convert. 25859 * @param {string} name Attribute name src, href etc. 25860 * @param {string/HTMLElement} elm Tag name or HTML DOM element depending on HTML or DOM insert. 25861 * @return {string} Converted URL string. 25862 */ 25863 convertURL: function(url, name, elm) { 25864 var self = this, settings = self.settings; 25865 25866 // Use callback instead 25867 if (settings.urlconverter_callback) { 25868 return self.execCallback('urlconverter_callback', url, elm, true, name); 25869 } 25870 25871 // Don't convert link href since thats the CSS files that gets loaded into the editor also skip local file URLs 25872 if (!settings.convert_urls || (elm && elm.nodeName == 'LINK') || url.indexOf('file:') === 0 || url.length === 0) { 25873 return url; 25874 } 25875 25876 // Convert to relative 25877 if (settings.relative_urls) { 25878 return self.documentBaseURI.toRelative(url); 25879 } 25880 25881 // Convert to absolute 25882 url = self.documentBaseURI.toAbsolute(url, settings.remove_script_host); 25883 25884 return url; 25885 }, 25886 25887 /** 25888 * Adds visual aid for tables, anchors etc so they can be more easily edited inside the editor. 25889 * 25890 * @method addVisual 25891 * @param {Element} elm Optional root element to loop though to find tables etc that needs the visual aid. 25892 */ 25893 addVisual: function(elm) { 25894 var self = this, settings = self.settings, dom = self.dom, cls; 25895 25896 elm = elm || self.getBody(); 25897 25898 if (self.hasVisual === undefined) { 25899 self.hasVisual = settings.visual; 25900 } 25901 25902 each(dom.select('table,a', elm), function(elm) { 25903 var value; 25904 25905 switch (elm.nodeName) { 25906 case 'TABLE': 25907 cls = settings.visual_table_class || 'mce-item-table'; 25908 value = dom.getAttrib(elm, 'border'); 25909 25910 if (!value || value == '0') { 25911 if (self.hasVisual) { 25912 dom.addClass(elm, cls); 25913 } else { 25914 dom.removeClass(elm, cls); 25915 } 25916 } 25917 25918 return; 25919 25920 case 'A': 25921 if (!dom.getAttrib(elm, 'href', false)) { 25922 value = dom.getAttrib(elm, 'name') || elm.id; 25923 cls = settings.visual_anchor_class || 'mce-item-anchor'; 25924 25925 if (value) { 25926 if (self.hasVisual) { 25927 dom.addClass(elm, cls); 25928 } else { 25929 dom.removeClass(elm, cls); 25930 } 25931 } 25932 } 25933 25934 return; 25935 } 25936 }); 25937 25938 self.fire('VisualAid', {element: elm, hasVisual: self.hasVisual}); 25939 }, 25940 25941 /** 25942 * Removes the editor from the dom and tinymce collection. 25943 * 25944 * @method remove 25945 */ 25946 remove: function() { 25947 var self = this; 25948 25949 if (!self.removed) { 25950 self.removed = 1; 25951 self.save(); 25952 25953 // Remove any hidden input 25954 if (self.hasHiddenInput) { 25955 DOM.remove(self.getElement().nextSibling); 25956 } 25957 25958 if (!self.inline) { 25959 // IE 9 has a bug where the selection stops working if you place the 25960 // caret inside the editor then remove the iframe 25961 if (ie && ie < 10) { 25962 self.getDoc().execCommand('SelectAll', false, null); 25963 } 25964 25965 DOM.setStyle(self.id, 'display', self.orgDisplay); 25966 self.getBody().onload = null; // Prevent #6816 25967 25968 // Don't clear the window or document if content editable 25969 // is enabled since other instances might still be present 25970 Event.unbind(self.getWin()); 25971 Event.unbind(self.getDoc()); 25972 } 25973 25974 var elm = self.getContainer(); 25975 Event.unbind(self.getBody()); 25976 Event.unbind(elm); 25977 25978 self.fire('remove'); 25979 25980 self.editorManager.remove(self); 25981 DOM.remove(elm); 25982 self.destroy(); 25983 } 25984 }, 25985 25986 /** 25987 * Destroys the editor instance by removing all events, element references or other resources 25988 * that could leak memory. This method will be called automatically when the page is unloaded 25989 * but you can also call it directly if you know what you are doing. 25990 * 25991 * @method destroy 25992 * @param {Boolean} automatic Optional state if the destroy is an automatic destroy or user called one. 25993 */ 25994 destroy: function(automatic) { 25995 var self = this, form; 25996 25997 // One time is enough 25998 if (self.destroyed) { 25999 return; 26000 } 26001 26002 // If user manually calls destroy and not remove 26003 // Users seems to have logic that calls destroy instead of remove 26004 if (!automatic && !self.removed) { 26005 self.remove(); 26006 return; 26007 } 26008 26009 // We must unbind on Gecko since it would otherwise produce the pesky "attempt 26010 // to run compile-and-go script on a cleared scope" message 26011 if (automatic && isGecko) { 26012 Event.unbind(self.getDoc()); 26013 Event.unbind(self.getWin()); 26014 Event.unbind(self.getBody()); 26015 } 26016 26017 if (!automatic) { 26018 self.editorManager.off('beforeunload', self._beforeUnload); 26019 26020 // Manual destroy 26021 if (self.theme && self.theme.destroy) { 26022 self.theme.destroy(); 26023 } 26024 26025 // Destroy controls, selection and dom 26026 self.selection.destroy(); 26027 self.dom.destroy(); 26028 } 26029 26030 form = self.formElement; 26031 if (form) { 26032 if (form._mceOldSubmit) { 26033 form.submit = form._mceOldSubmit; 26034 form._mceOldSubmit = null; 26035 } 26036 26037 DOM.unbind(form, 'submit reset', self.formEventDelegate); 26038 } 26039 26040 self.contentAreaContainer = self.formElement = self.container = self.editorContainer = null; 26041 self.settings.content_element = self.bodyElement = self.contentDocument = self.contentWindow = null; 26042 26043 if (self.selection) { 26044 self.selection = self.selection.win = self.selection.dom = self.selection.dom.doc = null; 26045 } 26046 26047 self.destroyed = 1; 26048 }, 26049 26050 // Internal functions 26051 26052 _refreshContentEditable: function() { 26053 var self = this, body, parent; 26054 26055 // Check if the editor was hidden and the re-initalize contentEditable mode by removing and adding the body again 26056 if (self._isHidden()) { 26057 body = self.getBody(); 26058 parent = body.parentNode; 26059 26060 parent.removeChild(body); 26061 parent.appendChild(body); 26062 26063 body.focus(); 26064 } 26065 }, 26066 26067 _isHidden: function() { 26068 var sel; 26069 26070 if (!isGecko) { 26071 return 0; 26072 } 26073 26074 // Weird, wheres that cursor selection? 26075 sel = this.selection.getSel(); 26076 return (!sel || !sel.rangeCount || sel.rangeCount === 0); 26077 } 26078 }; 26079 26080 extend(Editor.prototype, EditorObservable); 26081 26082 return Editor; 26083 }); 26084 26085 // Included from: js/tinymce/classes/util/I18n.js 26086 26087 /** 26088 * I18n.js 26089 * 26090 * Copyright, Moxiecode Systems AB 26091 * Released under LGPL License. 26092 * 26093 * License: http://www.tinymce.com/license 26094 * Contributing: http://www.tinymce.com/contributing 26095 */ 26096 26097 /** 26098 * I18n class that handles translation of TinyMCE UI. 26099 * Uses po style with csharp style parameters. 26100 * 26101 * @class tinymce.util.I18n 26102 */ 26103 define("tinymce/util/I18n", [], function() { 26104 "use strict"; 26105 26106 var data = {}; 26107 26108 return { 26109 /** 26110 * Property gets set to true if a RTL language pack was loaded. 26111 * 26112 * @property rtl 26113 * @type Boolean 26114 */ 26115 rtl: false, 26116 26117 /** 26118 * Adds translations for a specific language code. 26119 * 26120 * @method add 26121 * @param {String} code Language code like sv_SE. 26122 * @param {Array} items Name/value array with English en_US to sv_SE. 26123 */ 26124 add: function(code, items) { 26125 for (var name in items) { 26126 data[name] = items[name]; 26127 } 26128 26129 this.rtl = this.rtl || data._dir === 'rtl'; 26130 }, 26131 26132 /** 26133 * Translates the specified text. 26134 * 26135 * It has a few formats: 26136 * I18n.translate("Text"); 26137 * I18n.translate(["Text {0}/{1}", 0, 1]); 26138 * I18n.translate({raw: "Raw string"}); 26139 * 26140 * @method translate 26141 * @param {String/Object/Array} text Text to translate. 26142 * @return {String} String that got translated. 26143 */ 26144 translate: function(text) { 26145 if (typeof(text) == "undefined") { 26146 return text; 26147 } 26148 26149 if (typeof(text) != "string" && text.raw) { 26150 return text.raw; 26151 } 26152 26153 if (text.push) { 26154 var values = text.slice(1); 26155 26156 text = (data[text[0]] || text[0]).replace(/\{([^\}]+)\}/g, function(match1, match2) { 26157 return values[match2]; 26158 }); 26159 } 26160 26161 return data[text] || text; 26162 }, 26163 26164 data: data 26165 }; 26166 }); 26167 26168 // Included from: js/tinymce/classes/FocusManager.js 26169 26170 /** 26171 * FocusManager.js 26172 * 26173 * Copyright, Moxiecode Systems AB 26174 * Released under LGPL License. 26175 * 26176 * License: http://www.tinymce.com/license 26177 * Contributing: http://www.tinymce.com/contributing 26178 */ 26179 26180 /** 26181 * This class manages the focus/blur state of the editor. This class is needed since some 26182 * browsers fire false focus/blur states when the selection is moved to a UI dialog or similar. 26183 * 26184 * This class will fire two events focus and blur on the editor instances that got affected. 26185 * It will also handle the restore of selection when the focus is lost and returned. 26186 * 26187 * @class tinymce.FocusManager 26188 */ 26189 define("tinymce/FocusManager", [ 26190 "tinymce/dom/DOMUtils", 26191 "tinymce/Env" 26192 ], function(DOMUtils, Env) { 26193 var selectionChangeHandler, documentFocusInHandler, documentMouseUpHandler, DOM = DOMUtils.DOM; 26194 26195 /** 26196 * Constructs a new focus manager instance. 26197 * 26198 * @constructor FocusManager 26199 * @param {tinymce.EditorManager} editorManager Editor manager instance to handle focus for. 26200 */ 26201 function FocusManager(editorManager) { 26202 function getActiveElement() { 26203 try { 26204 return document.activeElement; 26205 } catch (ex) { 26206 // IE sometimes fails to get the activeElement when resizing table 26207 // TODO: Investigate this 26208 return document.body; 26209 } 26210 } 26211 26212 // We can't store a real range on IE 11 since it gets mutated so we need to use a bookmark object 26213 // TODO: Move this to a separate range utils class since it's it's logic is present in Selection as well. 26214 function createBookmark(dom, rng) { 26215 if (rng && rng.startContainer) { 26216 // Verify that the range is within the root of the editor 26217 if (!dom.isChildOf(rng.startContainer, dom.getRoot()) || !dom.isChildOf(rng.endContainer, dom.getRoot())) { 26218 return; 26219 } 26220 26221 return { 26222 startContainer: rng.startContainer, 26223 startOffset: rng.startOffset, 26224 endContainer: rng.endContainer, 26225 endOffset: rng.endOffset 26226 }; 26227 } 26228 26229 return rng; 26230 } 26231 26232 function bookmarkToRng(editor, bookmark) { 26233 var rng; 26234 26235 if (bookmark.startContainer) { 26236 rng = editor.getDoc().createRange(); 26237 rng.setStart(bookmark.startContainer, bookmark.startOffset); 26238 rng.setEnd(bookmark.endContainer, bookmark.endOffset); 26239 } else { 26240 rng = bookmark; 26241 } 26242 26243 return rng; 26244 } 26245 26246 function isUIElement(elm) { 26247 return !!DOM.getParent(elm, FocusManager.isEditorUIElement); 26248 } 26249 26250 function registerEvents(e) { 26251 var editor = e.editor; 26252 26253 editor.on('init', function() { 26254 // Gecko/WebKit has ghost selections in iframes and IE only has one selection per browser tab 26255 if (editor.inline || Env.ie) { 26256 // On other browsers take snapshot on nodechange in inline mode since they have Ghost selections for iframes 26257 editor.on('nodechange keyup', function() { 26258 var node = document.activeElement; 26259 26260 // IE 11 reports active element as iframe not body of iframe 26261 if (node && node.id == editor.id + '_ifr') { 26262 node = editor.getBody(); 26263 } 26264 26265 if (editor.dom.isChildOf(node, editor.getBody())) { 26266 editor.lastRng = editor.selection.getRng(); 26267 } 26268 }); 26269 26270 // Handles the issue with WebKit not retaining selection within inline document 26271 // If the user releases the mouse out side the body since a mouse up event wont occur on the body 26272 if (Env.webkit && !selectionChangeHandler) { 26273 selectionChangeHandler = function() { 26274 var activeEditor = editorManager.activeEditor; 26275 26276 if (activeEditor && activeEditor.selection) { 26277 var rng = activeEditor.selection.getRng(); 26278 26279 // Store when it's non collapsed 26280 if (rng && !rng.collapsed) { 26281 editor.lastRng = rng; 26282 } 26283 } 26284 }; 26285 26286 DOM.bind(document, 'selectionchange', selectionChangeHandler); 26287 } 26288 } 26289 }); 26290 26291 editor.on('setcontent', function() { 26292 editor.lastRng = null; 26293 }); 26294 26295 // Remove last selection bookmark on mousedown see #6305 26296 editor.on('mousedown', function() { 26297 editor.selection.lastFocusBookmark = null; 26298 }); 26299 26300 editor.on('focusin', function() { 26301 var focusedEditor = editorManager.focusedEditor; 26302 26303 if (editor.selection.lastFocusBookmark) { 26304 editor.selection.setRng(bookmarkToRng(editor, editor.selection.lastFocusBookmark)); 26305 editor.selection.lastFocusBookmark = null; 26306 } 26307 26308 if (focusedEditor != editor) { 26309 if (focusedEditor) { 26310 focusedEditor.fire('blur', {focusedEditor: editor}); 26311 } 26312 26313 editorManager.activeEditor = editor; 26314 editorManager.focusedEditor = editor; 26315 editor.fire('focus', {blurredEditor: focusedEditor}); 26316 editor.focus(true); 26317 } 26318 26319 editor.lastRng = null; 26320 }); 26321 26322 editor.on('focusout', function() { 26323 window.setTimeout(function() { 26324 var focusedEditor = editorManager.focusedEditor; 26325 26326 // Still the same editor the the blur was outside any editor UI 26327 if (!isUIElement(getActiveElement()) && focusedEditor == editor) { 26328 editor.fire('blur', {focusedEditor: null}); 26329 editorManager.focusedEditor = null; 26330 26331 // Make sure selection is valid could be invalid if the editor is blured and removed before the timeout occurs 26332 if (editor.selection) { 26333 editor.selection.lastFocusBookmark = null; 26334 } 26335 } 26336 }, 0); 26337 }); 26338 26339 // Check if focus is moved to an element outside the active editor by checking if the target node 26340 // isn't within the body of the activeEditor nor a UI element such as a dialog child control 26341 if (!documentFocusInHandler) { 26342 documentFocusInHandler = function(e) { 26343 var activeEditor = editorManager.activeEditor; 26344 26345 if (activeEditor && e.target.ownerDocument == document) { 26346 // Check to make sure we have a valid selection 26347 if (activeEditor.selection) { 26348 activeEditor.selection.lastFocusBookmark = createBookmark(activeEditor.dom, activeEditor.lastRng); 26349 } 26350 26351 // Fire a blur event if the element isn't a UI element 26352 if (!isUIElement(e.target) && editorManager.focusedEditor == activeEditor) { 26353 activeEditor.fire('blur', {focusedEditor: null}); 26354 editorManager.focusedEditor = null; 26355 } 26356 } 26357 }; 26358 26359 DOM.bind(document, 'focusin', documentFocusInHandler); 26360 } 26361 26362 // Handle edge case when user starts the selection inside the editor and releases 26363 // the mouse outside the editor producing a new selection. This weird workaround is needed since 26364 // Gecko doesn't have the "selectionchange" event we need to do this. Fixes: #6843 26365 if (editor.inline && !documentMouseUpHandler) { 26366 documentMouseUpHandler = function(e) { 26367 var activeEditor = editorManager.activeEditor; 26368 26369 if (activeEditor.inline && !activeEditor.dom.isChildOf(e.target, activeEditor.getBody())) { 26370 var rng = activeEditor.selection.getRng(); 26371 26372 if (!rng.collapsed) { 26373 activeEditor.lastRng = rng; 26374 } 26375 } 26376 }; 26377 26378 DOM.bind(document, 'mouseup', documentMouseUpHandler); 26379 } 26380 } 26381 26382 function unregisterDocumentEvents(e) { 26383 if (editorManager.focusedEditor == e.editor) { 26384 editorManager.focusedEditor = null; 26385 } 26386 26387 if (!editorManager.activeEditor) { 26388 DOM.unbind(document, 'selectionchange', selectionChangeHandler); 26389 DOM.unbind(document, 'focusin', documentFocusInHandler); 26390 DOM.unbind(document, 'mouseup', documentMouseUpHandler); 26391 selectionChangeHandler = documentFocusInHandler = documentMouseUpHandler = null; 26392 } 26393 } 26394 26395 editorManager.on('AddEditor', registerEvents); 26396 editorManager.on('RemoveEditor', unregisterDocumentEvents); 26397 } 26398 26399 /** 26400 * Returns true if the specified element is part of the UI for example an button or text input. 26401 * 26402 * @method isEditorUIElement 26403 * @param {Element} elm Element to check if it's part of the UI or not. 26404 * @return {Boolean} True/false state if the element is part of the UI or not. 26405 */ 26406 FocusManager.isEditorUIElement = function(elm) { 26407 // Needs to be converted to string since svg can have focus: #6776 26408 return elm.className.toString().indexOf('mce-') !== -1; 26409 }; 26410 26411 return FocusManager; 26412 }); 26413 26414 // Included from: js/tinymce/classes/EditorManager.js 26415 26416 /** 26417 * EditorManager.js 26418 * 26419 * Copyright, Moxiecode Systems AB 26420 * Released under LGPL License. 26421 * 26422 * License: http://www.tinymce.com/license 26423 * Contributing: http://www.tinymce.com/contributing 26424 */ 26425 26426 /** 26427 * This class used as a factory for manager for tinymce.Editor instances. 26428 * 26429 * @example 26430 * tinymce.EditorManager.init({}); 26431 * 26432 * @class tinymce.EditorManager 26433 * @mixes tinymce.util.Observable 26434 * @static 26435 */ 26436 define("tinymce/EditorManager", [ 26437 "tinymce/Editor", 26438 "tinymce/dom/DOMUtils", 26439 "tinymce/util/URI", 26440 "tinymce/Env", 26441 "tinymce/util/Tools", 26442 "tinymce/util/Observable", 26443 "tinymce/util/I18n", 26444 "tinymce/FocusManager" 26445 ], function(Editor, DOMUtils, URI, Env, Tools, Observable, I18n, FocusManager) { 26446 var DOM = DOMUtils.DOM; 26447 var explode = Tools.explode, each = Tools.each, extend = Tools.extend; 26448 var instanceCounter = 0, beforeUnloadDelegate, EditorManager; 26449 26450 function removeEditorFromList(editor) { 26451 var editors = EditorManager.editors, removedFromList; 26452 26453 delete editors[editor.id]; 26454 26455 for (var i = 0; i < editors.length; i++) { 26456 if (editors[i] == editor) { 26457 editors.splice(i, 1); 26458 removedFromList = true; 26459 break; 26460 } 26461 } 26462 26463 // Select another editor since the active one was removed 26464 if (EditorManager.activeEditor == editor) { 26465 EditorManager.activeEditor = editors[0]; 26466 } 26467 26468 // Clear focusedEditor if necessary, so that we don't try to blur the destroyed editor 26469 if (EditorManager.focusedEditor == editor) { 26470 EditorManager.focusedEditor = null; 26471 } 26472 26473 return removedFromList; 26474 } 26475 26476 function purgeDestroyedEditor(editor) { 26477 // User has manually destroyed the editor lets clean up the mess 26478 if (editor && !(editor.getContainer() || editor.getBody()).parentNode) { 26479 removeEditorFromList(editor); 26480 editor.destroy(true); 26481 editor = null; 26482 } 26483 26484 return editor; 26485 } 26486 26487 EditorManager = { 26488 /** 26489 * Major version of TinyMCE build. 26490 * 26491 * @property majorVersion 26492 * @type String 26493 */ 26494 majorVersion : '4', 26495 26496 /** 26497 * Minor version of TinyMCE build. 26498 * 26499 * @property minorVersion 26500 * @type String 26501 */ 26502 minorVersion : '0.25', 26503 26504 /** 26505 * Release date of TinyMCE build. 26506 * 26507 * @property releaseDate 26508 * @type String 26509 */ 26510 releaseDate: '2014-04-30', 26511 26512 /** 26513 * Collection of editor instances. 26514 * 26515 * @property editors 26516 * @type Object 26517 * @example 26518 * for (edId in tinymce.editors) 26519 * tinymce.editors[edId].save(); 26520 */ 26521 editors: [], 26522 26523 /** 26524 * Collection of language pack data. 26525 * 26526 * @property i18n 26527 * @type Object 26528 */ 26529 i18n: I18n, 26530 26531 /** 26532 * Currently active editor instance. 26533 * 26534 * @property activeEditor 26535 * @type tinymce.Editor 26536 * @example 26537 * tinyMCE.activeEditor.selection.getContent(); 26538 * tinymce.EditorManager.activeEditor.selection.getContent(); 26539 */ 26540 activeEditor: null, 26541 26542 setup: function() { 26543 var self = this, baseURL, documentBaseURL, suffix = "", preInit, src; 26544 26545 // Get base URL for the current document 26546 documentBaseURL = document.location.href.replace(/[\?#].*$/, '').replace(/[\/\\][^\/]+$/, ''); 26547 if (!/[\/\\]$/.test(documentBaseURL)) { 26548 documentBaseURL += '/'; 26549 } 26550 26551 // If tinymce is defined and has a base use that or use the old tinyMCEPreInit 26552 preInit = window.tinymce || window.tinyMCEPreInit; 26553 if (preInit) { 26554 baseURL = preInit.base || preInit.baseURL; 26555 suffix = preInit.suffix; 26556 } else { 26557 // Get base where the tinymce script is located 26558 var scripts = document.getElementsByTagName('script'); 26559 for (var i = 0; i < scripts.length; i++) { 26560 src = scripts[i].src; 26561 26562 // Script types supported: 26563 // tinymce.js tinymce.min.js tinymce.dev.js 26564 // tinymce.jquery.js tinymce.jquery.min.js tinymce.jquery.dev.js 26565 // tinymce.full.js tinymce.full.min.js tinymce.full.dev.js 26566 if (/tinymce(\.full|\.jquery|)(\.min|\.dev|)\.js/.test(src)) { 26567 if (src.indexOf('.min') != -1) { 26568 suffix = '.min'; 26569 } 26570 26571 baseURL = src.substring(0, src.lastIndexOf('/')); 26572 break; 26573 } 26574 } 26575 26576 // We didn't find any baseURL by looking at the script elements 26577 // Try to use the document.currentScript as a fallback 26578 if (!baseURL && document.currentScript) { 26579 src = document.currentScript.src; 26580 26581 if (src.indexOf('.min') != -1) { 26582 suffix = '.min'; 26583 } 26584 26585 baseURL = src.substring(0, src.lastIndexOf('/')); 26586 } 26587 } 26588 26589 /** 26590 * Base URL where the root directory if TinyMCE is located. 26591 * 26592 * @property baseURL 26593 * @type String 26594 */ 26595 self.baseURL = new URI(documentBaseURL).toAbsolute(baseURL); 26596 26597 /** 26598 * Document base URL where the current document is located. 26599 * 26600 * @property documentBaseURL 26601 * @type String 26602 */ 26603 self.documentBaseURL = documentBaseURL; 26604 26605 /** 26606 * Absolute baseURI for the installation path of TinyMCE. 26607 * 26608 * @property baseURI 26609 * @type tinymce.util.URI 26610 */ 26611 self.baseURI = new URI(self.baseURL); 26612 26613 /** 26614 * Current suffix to add to each plugin/theme that gets loaded for example ".min". 26615 * 26616 * @property suffix 26617 * @type String 26618 */ 26619 self.suffix = suffix; 26620 26621 self.focusManager = new FocusManager(self); 26622 }, 26623 26624 /** 26625 * Initializes a set of editors. This method will create editors based on various settings. 26626 * 26627 * @method init 26628 * @param {Object} settings Settings object to be passed to each editor instance. 26629 * @example 26630 * // Initializes a editor using the longer method 26631 * tinymce.EditorManager.init({ 26632 * some_settings : 'some value' 26633 * }); 26634 * 26635 * // Initializes a editor instance using the shorter version 26636 * tinyMCE.init({ 26637 * some_settings : 'some value' 26638 * }); 26639 */ 26640 init: function(settings) { 26641 var self = this, editors = [], editor; 26642 26643 function createId(elm) { 26644 var id = elm.id; 26645 26646 // Use element id, or unique name or generate a unique id 26647 if (!id) { 26648 id = elm.name; 26649 26650 if (id && !DOM.get(id)) { 26651 id = elm.name; 26652 } else { 26653 // Generate unique name 26654 id = DOM.uniqueId(); 26655 } 26656 26657 elm.setAttribute('id', id); 26658 } 26659 26660 return id; 26661 } 26662 26663 function createEditor(id, settings) { 26664 if (!purgeDestroyedEditor(self.get(id))) { 26665 var editor = new Editor(id, settings, self); 26666 editors.push(editor); 26667 editor.render(); 26668 } 26669 } 26670 26671 function execCallback(se, n, s) { 26672 var f = se[n]; 26673 26674 if (!f) { 26675 return; 26676 } 26677 26678 return f.apply(s || this, Array.prototype.slice.call(arguments, 2)); 26679 } 26680 26681 function hasClass(n, c) { 26682 return c.constructor === RegExp ? c.test(n.className) : DOM.hasClass(n, c); 26683 } 26684 26685 function readyHandler() { 26686 var l, co; 26687 26688 DOM.unbind(window, 'ready', readyHandler); 26689 26690 execCallback(settings, 'onpageload'); 26691 26692 if (settings.types) { 26693 // Process type specific selector 26694 each(settings.types, function(type) { 26695 each(DOM.select(type.selector), function(elm) { 26696 createEditor(createId(elm), extend({}, settings, type)); 26697 }); 26698 }); 26699 26700 return; 26701 } else if (settings.selector) { 26702 // Process global selector 26703 each(DOM.select(settings.selector), function(elm) { 26704 createEditor(createId(elm), settings); 26705 }); 26706 26707 return; 26708 } 26709 26710 // Fallback to old setting 26711 switch (settings.mode) { 26712 case "exact": 26713 l = settings.elements || ''; 26714 26715 if (l.length > 0) { 26716 each(explode(l), function(v) { 26717 if (DOM.get(v)) { 26718 editor = new Editor(v, settings, self); 26719 editors.push(editor); 26720 editor.render(); 26721 } else { 26722 each(document.forms, function(f) { 26723 each(f.elements, function(e) { 26724 if (e.name === v) { 26725 v = 'mce_editor_' + instanceCounter++; 26726 DOM.setAttrib(e, 'id', v); 26727 createEditor(v, settings); 26728 } 26729 }); 26730 }); 26731 } 26732 }); 26733 } 26734 break; 26735 26736 case "textareas": 26737 case "specific_textareas": 26738 each(DOM.select('textarea'), function(elm) { 26739 if (settings.editor_deselector && hasClass(elm, settings.editor_deselector)) { 26740 return; 26741 } 26742 26743 if (!settings.editor_selector || hasClass(elm, settings.editor_selector)) { 26744 createEditor(createId(elm), settings); 26745 } 26746 }); 26747 break; 26748 } 26749 26750 // Call onInit when all editors are initialized 26751 if (settings.oninit) { 26752 l = co = 0; 26753 26754 each(editors, function(ed) { 26755 co++; 26756 26757 if (!ed.initialized) { 26758 // Wait for it 26759 ed.on('init', function() { 26760 l++; 26761 26762 // All done 26763 if (l == co) { 26764 execCallback(settings, 'oninit'); 26765 } 26766 }); 26767 } else { 26768 l++; 26769 } 26770 26771 // All done 26772 if (l == co) { 26773 execCallback(settings, 'oninit'); 26774 } 26775 }); 26776 } 26777 } 26778 26779 self.settings = settings; 26780 26781 DOM.bind(window, 'ready', readyHandler); 26782 }, 26783 26784 /** 26785 * Returns a editor instance by id. 26786 * 26787 * @method get 26788 * @param {String/Number} id Editor instance id or index to return. 26789 * @return {tinymce.Editor} Editor instance to return. 26790 * @example 26791 * // Adds an onclick event to an editor by id (shorter version) 26792 * tinymce.get('mytextbox').on('click', function(e) { 26793 * ed.windowManager.alert('Hello world!'); 26794 * }); 26795 * 26796 * // Adds an onclick event to an editor by id (longer version) 26797 * tinymce.EditorManager.get('mytextbox').on('click', function(e) { 26798 * ed.windowManager.alert('Hello world!'); 26799 * }); 26800 */ 26801 get: function(id) { 26802 if (!arguments.length) { 26803 return this.editors; 26804 } 26805 26806 return id in this.editors ? this.editors[id] : null; 26807 }, 26808 26809 /** 26810 * Adds an editor instance to the editor collection. This will also set it as the active editor. 26811 * 26812 * @method add 26813 * @param {tinymce.Editor} editor Editor instance to add to the collection. 26814 * @return {tinymce.Editor} The same instance that got passed in. 26815 */ 26816 add: function(editor) { 26817 var self = this, editors = self.editors; 26818 26819 // Add named and index editor instance 26820 editors[editor.id] = editor; 26821 editors.push(editor); 26822 26823 self.activeEditor = editor; 26824 26825 /** 26826 * Fires when an editor is added to the EditorManager collection. 26827 * 26828 * @event AddEditor 26829 * @param {Object} e Event arguments. 26830 */ 26831 self.fire('AddEditor', {editor: editor}); 26832 26833 if (!beforeUnloadDelegate) { 26834 beforeUnloadDelegate = function() { 26835 self.fire('BeforeUnload'); 26836 }; 26837 26838 DOM.bind(window, 'beforeunload', beforeUnloadDelegate); 26839 } 26840 26841 return editor; 26842 }, 26843 26844 /** 26845 * Creates an editor instance and adds it to the EditorManager collection. 26846 * 26847 * @method createEditor 26848 * @param {String} id Instance id to use for editor. 26849 * @param {Object} settings Editor instance settings. 26850 * @return {tinymce.Editor} Editor instance that got created. 26851 */ 26852 createEditor: function(id, settings) { 26853 return this.add(new Editor(id, settings, this)); 26854 }, 26855 26856 /** 26857 * Removes a editor or editors form page. 26858 * 26859 * @example 26860 * // Remove all editors bound to divs 26861 * tinymce.remove('div'); 26862 * 26863 * // Remove all editors bound to textareas 26864 * tinymce.remove('textarea'); 26865 * 26866 * // Remove all editors 26867 * tinymce.remove(); 26868 * 26869 * // Remove specific instance by id 26870 * tinymce.remove('#id'); 26871 * 26872 * @method remove 26873 * @param {tinymce.Editor/String/Object} [selector] CSS selector or editor instance to remove. 26874 * @return {tinymce.Editor} The editor that got passed in will be return if it was found otherwise null. 26875 */ 26876 remove: function(selector) { 26877 var self = this, i, editors = self.editors, editor; 26878 26879 // Remove all editors 26880 if (!selector) { 26881 for (i = editors.length - 1; i >= 0; i--) { 26882 self.remove(editors[i]); 26883 } 26884 26885 return; 26886 } 26887 26888 // Remove editors by selector 26889 if (typeof(selector) == "string") { 26890 selector = selector.selector || selector; 26891 26892 each(DOM.select(selector), function(elm) { 26893 self.remove(editors[elm.id]); 26894 }); 26895 26896 return; 26897 } 26898 26899 // Remove specific editor 26900 editor = selector; 26901 26902 // Not in the collection 26903 if (!editors[editor.id]) { 26904 return null; 26905 } 26906 26907 /** 26908 * Fires when an editor is removed from EditorManager collection. 26909 * 26910 * @event RemoveEditor 26911 * @param {Object} e Event arguments. 26912 */ 26913 if (removeEditorFromList(editor)) { 26914 self.fire('RemoveEditor', {editor: editor}); 26915 } 26916 26917 if (!editors.length) { 26918 DOM.unbind(window, 'beforeunload', beforeUnloadDelegate); 26919 } 26920 26921 editor.remove(); 26922 26923 return editor; 26924 }, 26925 26926 /** 26927 * Executes a specific command on the currently active editor. 26928 * 26929 * @method execCommand 26930 * @param {String} c Command to perform for example Bold. 26931 * @param {Boolean} u Optional boolean state if a UI should be presented for the command or not. 26932 * @param {String} v Optional value parameter like for example an URL to a link. 26933 * @return {Boolean} true/false if the command was executed or not. 26934 */ 26935 execCommand: function(cmd, ui, value) { 26936 var self = this, editor = self.get(value); 26937 26938 // Manager commands 26939 switch (cmd) { 26940 case "mceAddEditor": 26941 if (!self.get(value)) { 26942 new Editor(value, self.settings, self).render(); 26943 } 26944 26945 return true; 26946 26947 case "mceRemoveEditor": 26948 if (editor) { 26949 editor.remove(); 26950 } 26951 26952 return true; 26953 26954 case 'mceToggleEditor': 26955 if (!editor) { 26956 self.execCommand('mceAddEditor', 0, value); 26957 return true; 26958 } 26959 26960 if (editor.isHidden()) { 26961 editor.show(); 26962 } else { 26963 editor.hide(); 26964 } 26965 26966 return true; 26967 } 26968 26969 // Run command on active editor 26970 if (self.activeEditor) { 26971 return self.activeEditor.execCommand(cmd, ui, value); 26972 } 26973 26974 return false; 26975 }, 26976 26977 /** 26978 * Calls the save method on all editor instances in the collection. This can be useful when a form is to be submitted. 26979 * 26980 * @method triggerSave 26981 * @example 26982 * // Saves all contents 26983 * tinyMCE.triggerSave(); 26984 */ 26985 triggerSave: function() { 26986 each(this.editors, function(editor) { 26987 editor.save(); 26988 }); 26989 }, 26990 26991 /** 26992 * Adds a language pack, this gets called by the loaded language files like en.js. 26993 * 26994 * @method addI18n 26995 * @param {String} code Optional language code. 26996 * @param {Object} items Name/value object with translations. 26997 */ 26998 addI18n: function(code, items) { 26999 I18n.add(code, items); 27000 }, 27001 27002 /** 27003 * Translates the specified string using the language pack items. 27004 * 27005 * @method translate 27006 * @param {String/Array/Object} text String to translate 27007 * @return {String} Translated string. 27008 */ 27009 translate: function(text) { 27010 return I18n.translate(text); 27011 } 27012 }; 27013 27014 extend(EditorManager, Observable); 27015 27016 EditorManager.setup(); 27017 27018 // Export EditorManager as tinymce/tinymce in global namespace 27019 window.tinymce = window.tinyMCE = EditorManager; 27020 27021 return EditorManager; 27022 }); 27023 27024 // Included from: js/tinymce/classes/LegacyInput.js 27025 27026 /** 27027 * LegacyInput.js 27028 * 27029 * Copyright, Moxiecode Systems AB 27030 * Released under LGPL License. 27031 * 27032 * License: http://www.tinymce.com/license 27033 * Contributing: http://www.tinymce.com/contributing 27034 */ 27035 27036 define("tinymce/LegacyInput", [ 27037 "tinymce/EditorManager", 27038 "tinymce/util/Tools" 27039 ], function(EditorManager, Tools) { 27040 var each = Tools.each, explode = Tools.explode; 27041 27042 EditorManager.on('AddEditor', function(e) { 27043 var editor = e.editor; 27044 27045 editor.on('preInit', function() { 27046 var filters, fontSizes, dom, settings = editor.settings; 27047 27048 function replaceWithSpan(node, styles) { 27049 each(styles, function(value, name) { 27050 if (value) { 27051 dom.setStyle(node, name, value); 27052 } 27053 }); 27054 27055 dom.rename(node, 'span'); 27056 } 27057 27058 function convert(e) { 27059 dom = editor.dom; 27060 27061 if (settings.convert_fonts_to_spans) { 27062 each(dom.select('font,u,strike', e.node), function(node) { 27063 filters[node.nodeName.toLowerCase()](dom, node); 27064 }); 27065 } 27066 } 27067 27068 if (settings.inline_styles) { 27069 fontSizes = explode(settings.font_size_legacy_values); 27070 27071 filters = { 27072 font: function(dom, node) { 27073 replaceWithSpan(node, { 27074 backgroundColor: node.style.backgroundColor, 27075 color: node.color, 27076 fontFamily: node.face, 27077 fontSize: fontSizes[parseInt(node.size, 10) - 1] 27078 }); 27079 }, 27080 27081 u: function(dom, node) { 27082 replaceWithSpan(node, { 27083 textDecoration: 'underline' 27084 }); 27085 }, 27086 27087 strike: function(dom, node) { 27088 replaceWithSpan(node, { 27089 textDecoration: 'line-through' 27090 }); 27091 } 27092 }; 27093 27094 editor.on('PreProcess SetContent', convert); 27095 } 27096 }); 27097 }); 27098 }); 27099 27100 // Included from: js/tinymce/classes/util/XHR.js 27101 27102 /** 27103 * XHR.js 27104 * 27105 * Copyright, Moxiecode Systems AB 27106 * Released under LGPL License. 27107 * 27108 * License: http://www.tinymce.com/license 27109 * Contributing: http://www.tinymce.com/contributing 27110 */ 27111 27112 /** 27113 * This class enables you to send XMLHTTPRequests cross browser. 27114 * @class tinymce.util.XHR 27115 * @static 27116 * @example 27117 * // Sends a low level Ajax request 27118 * tinymce.util.XHR.send({ 27119 * url: 'someurl', 27120 * success: function(text) { 27121 * console.debug(text); 27122 * } 27123 * }); 27124 */ 27125 define("tinymce/util/XHR", [], function() { 27126 return { 27127 /** 27128 * Sends a XMLHTTPRequest. 27129 * Consult the Wiki for details on what settings this method takes. 27130 * 27131 * @method send 27132 * @param {Object} settings Object will target URL, callbacks and other info needed to make the request. 27133 */ 27134 send: function(settings) { 27135 var xhr, count = 0; 27136 27137 function ready() { 27138 if (!settings.async || xhr.readyState == 4 || count++ > 10000) { 27139 if (settings.success && count < 10000 && xhr.status == 200) { 27140 settings.success.call(settings.success_scope, '' + xhr.responseText, xhr, settings); 27141 } else if (settings.error) { 27142 settings.error.call(settings.error_scope, count > 10000 ? 'TIMED_OUT' : 'GENERAL', xhr, settings); 27143 } 27144 27145 xhr = null; 27146 } else { 27147 setTimeout(ready, 10); 27148 } 27149 } 27150 27151 // Default settings 27152 settings.scope = settings.scope || this; 27153 settings.success_scope = settings.success_scope || settings.scope; 27154 settings.error_scope = settings.error_scope || settings.scope; 27155 settings.async = settings.async === false ? false : true; 27156 settings.data = settings.data || ''; 27157 27158 xhr = new XMLHttpRequest(); 27159 27160 if (xhr) { 27161 if (xhr.overrideMimeType) { 27162 xhr.overrideMimeType(settings.content_type); 27163 } 27164 27165 xhr.open(settings.type || (settings.data ? 'POST' : 'GET'), settings.url, settings.async); 27166 27167 if (settings.content_type) { 27168 xhr.setRequestHeader('Content-Type', settings.content_type); 27169 } 27170 27171 xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); 27172 27173 xhr.send(settings.data); 27174 27175 // Syncronous request 27176 if (!settings.async) { 27177 return ready(); 27178 } 27179 27180 // Wait for response, onReadyStateChange can not be used since it leaks memory in IE 27181 setTimeout(ready, 10); 27182 } 27183 } 27184 }; 27185 }); 27186 27187 // Included from: js/tinymce/classes/util/JSON.js 27188 27189 /** 27190 * JSON.js 27191 * 27192 * Copyright, Moxiecode Systems AB 27193 * Released under LGPL License. 27194 * 27195 * License: http://www.tinymce.com/license 27196 * Contributing: http://www.tinymce.com/contributing 27197 */ 27198 27199 /** 27200 * JSON parser and serializer class. 27201 * 27202 * @class tinymce.util.JSON 27203 * @static 27204 * @example 27205 * // JSON parse a string into an object 27206 * var obj = tinymce.util.JSON.parse(somestring); 27207 * 27208 * // JSON serialize a object into an string 27209 * var str = tinymce.util.JSON.serialize(obj); 27210 */ 27211 define("tinymce/util/JSON", [], function() { 27212 function serialize(o, quote) { 27213 var i, v, t, name; 27214 27215 quote = quote || '"'; 27216 27217 if (o === null) { 27218 return 'null'; 27219 } 27220 27221 t = typeof o; 27222 27223 if (t == 'string') { 27224 v = '\bb\tt\nn\ff\rr\""\'\'\\\\'; 27225 27226 return quote + o.replace(/([\u0080-\uFFFF\x00-\x1f\"\'\\])/g, function(a, b) { 27227 // Make sure single quotes never get encoded inside double quotes for JSON compatibility 27228 if (quote === '"' && a === "'") { 27229 return a; 27230 } 27231 27232 i = v.indexOf(b); 27233 27234 if (i + 1) { 27235 return '\\' + v.charAt(i + 1); 27236 } 27237 27238 a = b.charCodeAt().toString(16); 27239 27240 return '\\u' + '0000'.substring(a.length) + a; 27241 }) + quote; 27242 } 27243 27244 if (t == 'object') { 27245 if (o.hasOwnProperty && Object.prototype.toString.call(o) === '[object Array]') { 27246 for (i = 0, v = '['; i < o.length; i++) { 27247 v += (i > 0 ? ',' : '') + serialize(o[i], quote); 27248 } 27249 27250 return v + ']'; 27251 } 27252 27253 v = '{'; 27254 27255 for (name in o) { 27256 if (o.hasOwnProperty(name)) { 27257 v += typeof o[name] != 'function' ? (v.length > 1 ? ',' + quote : quote) + name + 27258 quote + ':' + serialize(o[name], quote) : ''; 27259 } 27260 } 27261 27262 return v + '}'; 27263 } 27264 27265 return '' + o; 27266 } 27267 27268 return { 27269 /** 27270 * Serializes the specified object as a JSON string. 27271 * 27272 * @method serialize 27273 * @param {Object} obj Object to serialize as a JSON string. 27274 * @param {String} quote Optional quote string defaults to ". 27275 * @return {string} JSON string serialized from input. 27276 */ 27277 serialize: serialize, 27278 27279 /** 27280 * Unserializes/parses the specified JSON string into a object. 27281 * 27282 * @method parse 27283 * @param {string} s JSON String to parse into a JavaScript object. 27284 * @return {Object} Object from input JSON string or undefined if it failed. 27285 */ 27286 parse: function(text) { 27287 try { 27288 // Trick uglify JS 27289 return window[String.fromCharCode(101) + 'val']('(' + text + ')'); 27290 } catch (ex) { 27291 // Ignore 27292 } 27293 } 27294 27295 /**#@-*/ 27296 }; 27297 }); 27298 27299 // Included from: js/tinymce/classes/util/JSONRequest.js 27300 27301 /** 27302 * JSONRequest.js 27303 * 27304 * Copyright, Moxiecode Systems AB 27305 * Released under LGPL License. 27306 * 27307 * License: http://www.tinymce.com/license 27308 * Contributing: http://www.tinymce.com/contributing 27309 */ 27310 27311 /** 27312 * This class enables you to use JSON-RPC to call backend methods. 27313 * 27314 * @class tinymce.util.JSONRequest 27315 * @example 27316 * var json = new tinymce.util.JSONRequest({ 27317 * url: 'somebackend.php' 27318 * }); 27319 * 27320 * // Send RPC call 1 27321 * json.send({ 27322 * method: 'someMethod1', 27323 * params: ['a', 'b'], 27324 * success: function(result) { 27325 * console.dir(result); 27326 * } 27327 * }); 27328 * 27329 * // Send RPC call 2 27330 * json.send({ 27331 * method: 'someMethod2', 27332 * params: ['a', 'b'], 27333 * success: function(result) { 27334 * console.dir(result); 27335 * } 27336 * }); 27337 */ 27338 define("tinymce/util/JSONRequest", [ 27339 "tinymce/util/JSON", 27340 "tinymce/util/XHR", 27341 "tinymce/util/Tools" 27342 ], function(JSON, XHR, Tools) { 27343 var extend = Tools.extend; 27344 27345 function JSONRequest(settings) { 27346 this.settings = extend({}, settings); 27347 this.count = 0; 27348 } 27349 27350 /** 27351 * Simple helper function to send a JSON-RPC request without the need to initialize an object. 27352 * Consult the Wiki API documentation for more details on what you can pass to this function. 27353 * 27354 * @method sendRPC 27355 * @static 27356 * @param {Object} o Call object where there are three field id, method and params this object should also contain callbacks etc. 27357 */ 27358 JSONRequest.sendRPC = function(o) { 27359 return new JSONRequest().send(o); 27360 }; 27361 27362 JSONRequest.prototype = { 27363 /** 27364 * Sends a JSON-RPC call. Consult the Wiki API documentation for more details on what you can pass to this function. 27365 * 27366 * @method send 27367 * @param {Object} args Call object where there are three field id, method and params this object should also contain callbacks etc. 27368 */ 27369 send: function(args) { 27370 var ecb = args.error, scb = args.success; 27371 27372 args = extend(this.settings, args); 27373 27374 args.success = function(c, x) { 27375 c = JSON.parse(c); 27376 27377 if (typeof(c) == 'undefined') { 27378 c = { 27379 error : 'JSON Parse error.' 27380 }; 27381 } 27382 27383 if (c.error) { 27384 ecb.call(args.error_scope || args.scope, c.error, x); 27385 } else { 27386 scb.call(args.success_scope || args.scope, c.result); 27387 } 27388 }; 27389 27390 args.error = function(ty, x) { 27391 if (ecb) { 27392 ecb.call(args.error_scope || args.scope, ty, x); 27393 } 27394 }; 27395 27396 args.data = JSON.serialize({ 27397 id: args.id || 'c' + (this.count++), 27398 method: args.method, 27399 params: args.params 27400 }); 27401 27402 // JSON content type for Ruby on rails. Bug: #1883287 27403 args.content_type = 'application/json'; 27404 27405 XHR.send(args); 27406 } 27407 }; 27408 27409 return JSONRequest; 27410 }); 27411 27412 // Included from: js/tinymce/classes/util/JSONP.js 27413 27414 /** 27415 * JSONP.js 27416 * 27417 * Copyright, Moxiecode Systems AB 27418 * Released under LGPL License. 27419 * 27420 * License: http://www.tinymce.com/license 27421 * Contributing: http://www.tinymce.com/contributing 27422 */ 27423 27424 define("tinymce/util/JSONP", [ 27425 "tinymce/dom/DOMUtils" 27426 ], function(DOMUtils) { 27427 return { 27428 callbacks: {}, 27429 count: 0, 27430 27431 send: function(settings) { 27432 var self = this, dom = DOMUtils.DOM, count = settings.count !== undefined ? settings.count : self.count; 27433 var id = 'tinymce_jsonp_' + count; 27434 27435 self.callbacks[count] = function(json) { 27436 dom.remove(id); 27437 delete self.callbacks[count]; 27438 27439 settings.callback(json); 27440 }; 27441 27442 dom.add(dom.doc.body, 'script', { 27443 id: id, 27444 src: settings.url, 27445 type: 'text/javascript' 27446 }); 27447 27448 self.count++; 27449 } 27450 }; 27451 }); 27452 27453 // Included from: js/tinymce/classes/util/LocalStorage.js 27454 27455 /** 27456 * LocalStorage.js 27457 * 27458 * Copyright, Moxiecode Systems AB 27459 * Released under LGPL License. 27460 * 27461 * License: http://www.tinymce.com/license 27462 * Contributing: http://www.tinymce.com/contributing 27463 */ 27464 27465 /** 27466 * This class will simulate LocalStorage on IE 7 and return the native version on modern browsers. 27467 * Storage is done using userData on IE 7 and a special serialization format. The format is designed 27468 * to be as small as possible by making sure that the keys and values doesn't need to be encoded. This 27469 * makes it possible to store for example HTML data. 27470 * 27471 * Storage format for userData: 27472 * <base 32 key length>,<key string>,<base 32 value length>,<value>,... 27473 * 27474 * For example this data key1=value1,key2=value2 would be: 27475 * 4,key1,6,value1,4,key2,6,value2 27476 * 27477 * @class tinymce.util.LocalStorage 27478 * @static 27479 * @version 4.0 27480 * @example 27481 * tinymce.util.LocalStorage.setItem('key', 'value'); 27482 * var value = tinymce.util.LocalStorage.getItem('key'); 27483 */ 27484 define("tinymce/util/LocalStorage", [], function() { 27485 var LocalStorage, storageElm, items, keys, userDataKey, hasOldIEDataSupport; 27486 27487 // Check for native support 27488 try { 27489 if (window.localStorage) { 27490 return localStorage; 27491 } 27492 } catch (ex) { 27493 // Ignore 27494 } 27495 27496 userDataKey = "tinymce"; 27497 storageElm = document.documentElement; 27498 hasOldIEDataSupport = !!storageElm.addBehavior; 27499 27500 if (hasOldIEDataSupport) { 27501 storageElm.addBehavior('#default#userData'); 27502 } 27503 27504 /** 27505 * Gets the keys names and updates LocalStorage.length property. Since IE7 doesn't have any getters/setters. 27506 */ 27507 function updateKeys() { 27508 keys = []; 27509 27510 for (var key in items) { 27511 keys.push(key); 27512 } 27513 27514 LocalStorage.length = keys.length; 27515 } 27516 27517 /** 27518 * Loads the userData string and parses it into the items structure. 27519 */ 27520 function load() { 27521 var key, data, value, pos = 0; 27522 27523 items = {}; 27524 27525 // localStorage can be disabled on WebKit/Gecko so make a dummy storage 27526 if (!hasOldIEDataSupport) { 27527 return; 27528 } 27529 27530 function next(end) { 27531 var value, nextPos; 27532 27533 nextPos = end !== undefined ? pos + end : data.indexOf(',', pos); 27534 if (nextPos === -1 || nextPos > data.length) { 27535 return null; 27536 } 27537 27538 value = data.substring(pos, nextPos); 27539 pos = nextPos + 1; 27540 27541 return value; 27542 } 27543 27544 storageElm.load(userDataKey); 27545 data = storageElm.getAttribute(userDataKey) || ''; 27546 27547 do { 27548 var offset = next(); 27549 if (offset === null) { 27550 break; 27551 } 27552 27553 key = next(parseInt(offset, 32) || 0); 27554 if (key !== null) { 27555 offset = next(); 27556 if (offset === null) { 27557 break; 27558 } 27559 27560 value = next(parseInt(offset, 32) || 0); 27561 27562 if (key) { 27563 items[key] = value; 27564 } 27565 } 27566 } while (key !== null); 27567 27568 updateKeys(); 27569 } 27570 27571 /** 27572 * Saves the items structure into a the userData format. 27573 */ 27574 function save() { 27575 var value, data = ''; 27576 27577 // localStorage can be disabled on WebKit/Gecko so make a dummy storage 27578 if (!hasOldIEDataSupport) { 27579 return; 27580 } 27581 27582 for (var key in items) { 27583 value = items[key]; 27584 data += (data ? ',' : '') + key.length.toString(32) + ',' + key + ',' + value.length.toString(32) + ',' + value; 27585 } 27586 27587 storageElm.setAttribute(userDataKey, data); 27588 27589 try { 27590 storageElm.save(userDataKey); 27591 } catch (ex) { 27592 // Ignore disk full 27593 } 27594 27595 updateKeys(); 27596 } 27597 27598 LocalStorage = { 27599 /** 27600 * Length of the number of items in storage. 27601 * 27602 * @property length 27603 * @type Number 27604 * @return {Number} Number of items in storage. 27605 */ 27606 //length:0, 27607 27608 /** 27609 * Returns the key name by index. 27610 * 27611 * @method key 27612 * @param {Number} index Index of key to return. 27613 * @return {String} Key value or null if it wasn't found. 27614 */ 27615 key: function(index) { 27616 return keys[index]; 27617 }, 27618 27619 /** 27620 * Returns the value if the specified key or null if it wasn't found. 27621 * 27622 * @method getItem 27623 * @param {String} key Key of item to retrive. 27624 * @return {String} Value of the specified item or null if it wasn't found. 27625 */ 27626 getItem: function(key) { 27627 return key in items ? items[key] : null; 27628 }, 27629 27630 /** 27631 * Sets the value of the specified item by it's key. 27632 * 27633 * @method setItem 27634 * @param {String} key Key of the item to set. 27635 * @param {String} value Value of the item to set. 27636 */ 27637 setItem: function(key, value) { 27638 items[key] = "" + value; 27639 save(); 27640 }, 27641 27642 /** 27643 * Removes the specified item by key. 27644 * 27645 * @method removeItem 27646 * @param {String} key Key of item to remove. 27647 */ 27648 removeItem: function(key) { 27649 delete items[key]; 27650 save(); 27651 }, 27652 27653 /** 27654 * Removes all items. 27655 * 27656 * @method clear 27657 */ 27658 clear: function() { 27659 items = {}; 27660 save(); 27661 } 27662 }; 27663 27664 load(); 27665 27666 return LocalStorage; 27667 }); 27668 27669 // Included from: js/tinymce/classes/Compat.js 27670 27671 /** 27672 * Compat.js 27673 * 27674 * Copyright, Moxiecode Systems AB 27675 * Released under LGPL License. 27676 * 27677 * License: http://www.tinymce.com/license 27678 * Contributing: http://www.tinymce.com/contributing 27679 */ 27680 27681 /** 27682 * TinyMCE core class. 27683 * 27684 * @static 27685 * @class tinymce 27686 * @borrow-members tinymce.EditorManager 27687 * @borrow-members tinymce.util.Tools 27688 */ 27689 define("tinymce/Compat", [ 27690 "tinymce/dom/DOMUtils", 27691 "tinymce/dom/EventUtils", 27692 "tinymce/dom/ScriptLoader", 27693 "tinymce/AddOnManager", 27694 "tinymce/util/Tools", 27695 "tinymce/Env" 27696 ], function(DOMUtils, EventUtils, ScriptLoader, AddOnManager, Tools, Env) { 27697 var tinymce = window.tinymce; 27698 27699 /** 27700 * @property {tinymce.dom.DOMUtils} DOM Global DOM instance. 27701 * @property {tinymce.dom.ScriptLoader} ScriptLoader Global ScriptLoader instance. 27702 * @property {tinymce.AddOnManager} PluginManager Global PluginManager instance. 27703 * @property {tinymce.AddOnManager} ThemeManager Global ThemeManager instance. 27704 */ 27705 tinymce.DOM = DOMUtils.DOM; 27706 tinymce.ScriptLoader = ScriptLoader.ScriptLoader; 27707 tinymce.PluginManager = AddOnManager.PluginManager; 27708 tinymce.ThemeManager = AddOnManager.ThemeManager; 27709 27710 tinymce.dom = tinymce.dom || {}; 27711 tinymce.dom.Event = EventUtils.Event; 27712 27713 Tools.each(Tools, function(func, key) { 27714 tinymce[key] = func; 27715 }); 27716 27717 Tools.each('isOpera isWebKit isIE isGecko isMac'.split(' '), function(name) { 27718 tinymce[name] = Env[name.substr(2).toLowerCase()]; 27719 }); 27720 27721 return {}; 27722 }); 27723 27724 // Describe the different namespaces 27725 27726 /** 27727 * Root level namespace this contains classes directly releated to the TinyMCE editor. 27728 * 27729 * @namespace tinymce 27730 */ 27731 27732 /** 27733 * Contains classes for handling the browsers DOM. 27734 * 27735 * @namespace tinymce.dom 27736 */ 27737 27738 /** 27739 * Contains html parser and serializer logic. 27740 * 27741 * @namespace tinymce.html 27742 */ 27743 27744 /** 27745 * Contains the different UI types such as buttons, listboxes etc. 27746 * 27747 * @namespace tinymce.ui 27748 */ 27749 27750 /** 27751 * Contains various utility classes such as json parser, cookies etc. 27752 * 27753 * @namespace tinymce.util 27754 */ 27755 27756 // Included from: js/tinymce/classes/ui/Layout.js 27757 27758 /** 27759 * Layout.js 27760 * 27761 * Copyright, Moxiecode Systems AB 27762 * Released under LGPL License. 27763 * 27764 * License: http://www.tinymce.com/license 27765 * Contributing: http://www.tinymce.com/contributing 27766 */ 27767 27768 /** 27769 * Base layout manager class. 27770 * 27771 * @class tinymce.ui.Layout 27772 */ 27773 define("tinymce/ui/Layout", [ 27774 "tinymce/util/Class", 27775 "tinymce/util/Tools" 27776 ], function(Class, Tools) { 27777 "use strict"; 27778 27779 return Class.extend({ 27780 Defaults: { 27781 firstControlClass: 'first', 27782 lastControlClass: 'last' 27783 }, 27784 27785 /** 27786 * Constructs a layout instance with the specified settings. 27787 * 27788 * @constructor 27789 * @param {Object} settings Name/value object with settings. 27790 */ 27791 init: function(settings) { 27792 this.settings = Tools.extend({}, this.Defaults, settings); 27793 }, 27794 27795 /** 27796 * This method gets invoked before the layout renders the controls. 27797 * 27798 * @method preRender 27799 * @param {tinymce.ui.Container} container Container instance to preRender. 27800 */ 27801 preRender: function(container) { 27802 container.addClass(this.settings.containerClass, 'body'); 27803 }, 27804 27805 /** 27806 * Applies layout classes to the container. 27807 * 27808 * @private 27809 */ 27810 applyClasses: function(container) { 27811 var self = this, settings = self.settings, items, firstClass, lastClass; 27812 27813 items = container.items().filter(':visible'); 27814 firstClass = settings.firstControlClass; 27815 lastClass = settings.lastControlClass; 27816 27817 items.each(function(item) { 27818 item.removeClass(firstClass).removeClass(lastClass); 27819 27820 if (settings.controlClass) { 27821 item.addClass(settings.controlClass); 27822 } 27823 }); 27824 27825 items.eq(0).addClass(firstClass); 27826 items.eq(-1).addClass(lastClass); 27827 }, 27828 27829 /** 27830 * Renders the specified container and any layout specific HTML. 27831 * 27832 * @method renderHtml 27833 * @param {tinymce.ui.Container} container Container to render HTML for. 27834 */ 27835 renderHtml: function(container) { 27836 var self = this, settings = self.settings, items, html = ''; 27837 27838 items = container.items(); 27839 items.eq(0).addClass(settings.firstControlClass); 27840 items.eq(-1).addClass(settings.lastControlClass); 27841 27842 items.each(function(item) { 27843 if (settings.controlClass) { 27844 item.addClass(settings.controlClass); 27845 } 27846 27847 html += item.renderHtml(); 27848 }); 27849 27850 return html; 27851 }, 27852 27853 /** 27854 * Recalculates the positions of the controls in the specified container. 27855 * 27856 * @method recalc 27857 * @param {tinymce.ui.Container} container Container instance to recalc. 27858 */ 27859 recalc: function() { 27860 }, 27861 27862 /** 27863 * This method gets invoked after the layout renders the controls. 27864 * 27865 * @method postRender 27866 * @param {tinymce.ui.Container} container Container instance to postRender. 27867 */ 27868 postRender: function() { 27869 } 27870 }); 27871 }); 27872 27873 // Included from: js/tinymce/classes/ui/AbsoluteLayout.js 27874 27875 /** 27876 * AbsoluteLayout.js 27877 * 27878 * Copyright, Moxiecode Systems AB 27879 * Released under LGPL License. 27880 * 27881 * License: http://www.tinymce.com/license 27882 * Contributing: http://www.tinymce.com/contributing 27883 */ 27884 27885 /** 27886 * LayoutManager for absolute positioning. This layout manager is more of 27887 * a base class for other layouts but can be created and used directly. 27888 * 27889 * @-x-less AbsoluteLayout.less 27890 * @class tinymce.ui.AbsoluteLayout 27891 * @extends tinymce.ui.Layout 27892 */ 27893 define("tinymce/ui/AbsoluteLayout", [ 27894 "tinymce/ui/Layout" 27895 ], function(Layout) { 27896 "use strict"; 27897 27898 return Layout.extend({ 27899 Defaults: { 27900 containerClass: 'abs-layout', 27901 controlClass: 'abs-layout-item' 27902 }, 27903 27904 /** 27905 * Recalculates the positions of the controls in the specified container. 27906 * 27907 * @method recalc 27908 * @param {tinymce.ui.Container} container Container instance to recalc. 27909 */ 27910 recalc: function(container) { 27911 container.items().filter(':visible').each(function(ctrl) { 27912 var settings = ctrl.settings; 27913 27914 ctrl.layoutRect({ 27915 x: settings.x, 27916 y: settings.y, 27917 w: settings.w, 27918 h: settings.h 27919 }); 27920 27921 if (ctrl.recalc) { 27922 ctrl.recalc(); 27923 } 27924 }); 27925 }, 27926 27927 /** 27928 * Renders the specified container and any layout specific HTML. 27929 * 27930 * @method renderHtml 27931 * @param {tinymce.ui.Container} container Container to render HTML for. 27932 */ 27933 renderHtml: function(container) { 27934 return '<div id="' + container._id + '-absend" class="' + container.classPrefix + 'abs-end"></div>' + this._super(container); 27935 } 27936 }); 27937 }); 27938 27939 // Included from: js/tinymce/classes/ui/Tooltip.js 27940 27941 /** 27942 * Tooltip.js 27943 * 27944 * Copyright, Moxiecode Systems AB 27945 * Released under LGPL License. 27946 * 27947 * License: http://www.tinymce.com/license 27948 * Contributing: http://www.tinymce.com/contributing 27949 */ 27950 27951 /** 27952 * Creates a tooltip instance. 27953 * 27954 * @-x-less ToolTip.less 27955 * @class tinymce.ui.ToolTip 27956 * @extends tinymce.ui.Control 27957 * @mixes tinymce.ui.Movable 27958 */ 27959 define("tinymce/ui/Tooltip", [ 27960 "tinymce/ui/Control", 27961 "tinymce/ui/Movable" 27962 ], function(Control, Movable) { 27963 return Control.extend({ 27964 Mixins: [Movable], 27965 27966 Defaults: { 27967 classes: 'widget tooltip tooltip-n' 27968 }, 27969 27970 /** 27971 * Sets/gets the current label text. 27972 * 27973 * @method text 27974 * @param {String} [text] New label text. 27975 * @return {String|tinymce.ui.Tooltip} Current text or current label instance. 27976 */ 27977 text: function(value) { 27978 var self = this; 27979 27980 if (typeof(value) != "undefined") { 27981 self._value = value; 27982 27983 if (self._rendered) { 27984 self.getEl().lastChild.innerHTML = self.encode(value); 27985 } 27986 27987 return self; 27988 } 27989 27990 return self._value; 27991 }, 27992 27993 /** 27994 * Renders the control as a HTML string. 27995 * 27996 * @method renderHtml 27997 * @return {String} HTML representing the control. 27998 */ 27999 renderHtml: function() { 28000 var self = this, prefix = self.classPrefix; 28001 28002 return ( 28003 '<div id="' + self._id + '" class="' + self.classes() + '" role="presentation">' + 28004 '<div class="' + prefix + 'tooltip-arrow"></div>' + 28005 '<div class="' + prefix + 'tooltip-inner">' + self.encode(self._text) + '</div>' + 28006 '</div>' 28007 ); 28008 }, 28009 28010 /** 28011 * Repaints the control after a layout operation. 28012 * 28013 * @method repaint 28014 */ 28015 repaint: function() { 28016 var self = this, style, rect; 28017 28018 style = self.getEl().style; 28019 rect = self._layoutRect; 28020 28021 style.left = rect.x + 'px'; 28022 style.top = rect.y + 'px'; 28023 style.zIndex = 0xFFFF + 0xFFFF; 28024 } 28025 }); 28026 }); 28027 28028 // Included from: js/tinymce/classes/ui/Widget.js 28029 28030 /** 28031 * Widget.js 28032 * 28033 * Copyright, Moxiecode Systems AB 28034 * Released under LGPL License. 28035 * 28036 * License: http://www.tinymce.com/license 28037 * Contributing: http://www.tinymce.com/contributing 28038 */ 28039 28040 /** 28041 * Widget base class a widget is a control that has a tooltip and some basic states. 28042 * 28043 * @class tinymce.ui.Widget 28044 * @extends tinymce.ui.Control 28045 */ 28046 define("tinymce/ui/Widget", [ 28047 "tinymce/ui/Control", 28048 "tinymce/ui/Tooltip" 28049 ], function(Control, Tooltip) { 28050 "use strict"; 28051 28052 var tooltip; 28053 28054 var Widget = Control.extend({ 28055 /** 28056 * Constructs a instance with the specified settings. 28057 * 28058 * @constructor 28059 * @param {Object} settings Name/value object with settings. 28060 * @setting {String} tooltip Tooltip text to display when hovering. 28061 * @setting {Boolean} autofocus True if the control should be focused when rendered. 28062 * @setting {String} text Text to display inside widget. 28063 */ 28064 init: function(settings) { 28065 var self = this; 28066 28067 self._super(settings); 28068 settings = self.settings; 28069 self.canFocus = true; 28070 28071 if (settings.tooltip && Widget.tooltips !== false) { 28072 self.on('mouseenter', function(e) { 28073 var tooltip = self.tooltip().moveTo(-0xFFFF); 28074 28075 if (e.control == self) { 28076 var rel = tooltip.text(settings.tooltip).show().testMoveRel(self.getEl(), ['bc-tc', 'bc-tl', 'bc-tr']); 28077 28078 tooltip.toggleClass('tooltip-n', rel == 'bc-tc'); 28079 tooltip.toggleClass('tooltip-nw', rel == 'bc-tl'); 28080 tooltip.toggleClass('tooltip-ne', rel == 'bc-tr'); 28081 28082 tooltip.moveRel(self.getEl(), rel); 28083 } else { 28084 tooltip.hide(); 28085 } 28086 }); 28087 28088 self.on('mouseleave mousedown click', function() { 28089 self.tooltip().hide(); 28090 }); 28091 } 28092 28093 self.aria('label', settings.ariaLabel || settings.tooltip); 28094 }, 28095 28096 /** 28097 * Returns the current tooltip instance. 28098 * 28099 * @method tooltip 28100 * @return {tinymce.ui.Tooltip} Tooltip instance. 28101 */ 28102 tooltip: function() { 28103 if (!tooltip) { 28104 tooltip = new Tooltip({type: 'tooltip'}); 28105 tooltip.renderTo(); 28106 } 28107 28108 return tooltip; 28109 }, 28110 28111 /** 28112 * Sets/gets the active state of the widget. 28113 * 28114 * @method active 28115 * @param {Boolean} [state] State if the control is active. 28116 * @return {Boolean|tinymce.ui.Widget} True/false or current widget instance. 28117 */ 28118 active: function(state) { 28119 var self = this, undef; 28120 28121 if (state !== undef) { 28122 self.aria('pressed', state); 28123 self.toggleClass('active', state); 28124 } 28125 28126 return self._super(state); 28127 }, 28128 28129 /** 28130 * Sets/gets the disabled state of the widget. 28131 * 28132 * @method disabled 28133 * @param {Boolean} [state] State if the control is disabled. 28134 * @return {Boolean|tinymce.ui.Widget} True/false or current widget instance. 28135 */ 28136 disabled: function(state) { 28137 var self = this, undef; 28138 28139 if (state !== undef) { 28140 self.aria('disabled', state); 28141 self.toggleClass('disabled', state); 28142 } 28143 28144 return self._super(state); 28145 }, 28146 28147 /** 28148 * Called after the control has been rendered. 28149 * 28150 * @method postRender 28151 */ 28152 postRender: function() { 28153 var self = this, settings = self.settings; 28154 28155 self._rendered = true; 28156 28157 self._super(); 28158 28159 if (!self.parent() && (settings.width || settings.height)) { 28160 self.initLayoutRect(); 28161 self.repaint(); 28162 } 28163 28164 if (settings.autofocus) { 28165 self.focus(); 28166 } 28167 }, 28168 28169 /** 28170 * Removes the current control from DOM and from UI collections. 28171 * 28172 * @method remove 28173 * @return {tinymce.ui.Control} Current control instance. 28174 */ 28175 remove: function() { 28176 this._super(); 28177 28178 if (tooltip) { 28179 tooltip.remove(); 28180 tooltip = null; 28181 } 28182 } 28183 }); 28184 28185 return Widget; 28186 }); 28187 28188 // Included from: js/tinymce/classes/ui/Button.js 28189 28190 /** 28191 * Button.js 28192 * 28193 * Copyright, Moxiecode Systems AB 28194 * Released under LGPL License. 28195 * 28196 * License: http://www.tinymce.com/license 28197 * Contributing: http://www.tinymce.com/contributing 28198 */ 28199 28200 /** 28201 * This class is used to create buttons. You can create them directly or through the Factory. 28202 * 28203 * @example 28204 * // Create and render a button to the body element 28205 * tinymce.ui.Factory.create({ 28206 * type: 'button', 28207 * text: 'My button' 28208 * }).renderTo(document.body); 28209 * 28210 * @-x-less Button.less 28211 * @class tinymce.ui.Button 28212 * @extends tinymce.ui.Widget 28213 */ 28214 define("tinymce/ui/Button", [ 28215 "tinymce/ui/Widget" 28216 ], function(Widget) { 28217 "use strict"; 28218 28219 return Widget.extend({ 28220 Defaults: { 28221 classes: "widget btn", 28222 role: "button" 28223 }, 28224 28225 /** 28226 * Constructs a new button instance with the specified settings. 28227 * 28228 * @constructor 28229 * @param {Object} settings Name/value object with settings. 28230 * @setting {String} size Size of the button small|medium|large. 28231 * @setting {String} image Image to use for icon. 28232 * @setting {String} icon Icon to use for button. 28233 */ 28234 init: function(settings) { 28235 var self = this, size; 28236 28237 self.on('click mousedown', function(e) { 28238 e.preventDefault(); 28239 }); 28240 28241 self._super(settings); 28242 size = settings.size; 28243 28244 if (settings.subtype) { 28245 self.addClass(settings.subtype); 28246 } 28247 28248 if (size) { 28249 self.addClass('btn-' + size); 28250 } 28251 }, 28252 28253 /** 28254 * Sets/gets the current button icon. 28255 * 28256 * @method icon 28257 * @param {String} [icon] New icon identifier. 28258 * @return {String|tinymce.ui.MenuButton} Current icon or current MenuButton instance. 28259 */ 28260 icon: function(icon) { 28261 var self = this, prefix = self.classPrefix; 28262 28263 if (typeof(icon) == 'undefined') { 28264 return self.settings.icon; 28265 } 28266 28267 self.settings.icon = icon; 28268 icon = icon ? prefix + 'ico ' + prefix + 'i-' + self.settings.icon : ''; 28269 28270 if (self._rendered) { 28271 var btnElm = self.getEl().firstChild, iconElm = btnElm.getElementsByTagName('i')[0]; 28272 28273 if (icon) { 28274 if (!iconElm || iconElm != btnElm.firstChild) { 28275 iconElm = document.createElement('i'); 28276 btnElm.insertBefore(iconElm, btnElm.firstChild); 28277 } 28278 28279 iconElm.className = icon; 28280 } else if (iconElm) { 28281 btnElm.removeChild(iconElm); 28282 } 28283 28284 self.text(self._text); // Set text again to fix whitespace between icon + text 28285 } 28286 28287 return self; 28288 }, 28289 28290 /** 28291 * Repaints the button for example after it's been resizes by a layout engine. 28292 * 28293 * @method repaint 28294 */ 28295 repaint: function() { 28296 var btnStyle = this.getEl().firstChild.style; 28297 28298 btnStyle.width = btnStyle.height = "100%"; 28299 28300 this._super(); 28301 }, 28302 28303 /** 28304 * Sets/gets the current button text. 28305 * 28306 * @method text 28307 * @param {String} [text] New button text. 28308 * @return {String|tinymce.ui.Button} Current text or current Button instance. 28309 */ 28310 text: function(text) { 28311 var self = this; 28312 28313 if (self._rendered) { 28314 var textNode = self.getEl().lastChild.lastChild; 28315 if (textNode) { 28316 textNode.data = self.translate(text); 28317 } 28318 } 28319 28320 return self._super(text); 28321 }, 28322 28323 /** 28324 * Renders the control as a HTML string. 28325 * 28326 * @method renderHtml 28327 * @return {String} HTML representing the control. 28328 */ 28329 renderHtml: function() { 28330 var self = this, id = self._id, prefix = self.classPrefix; 28331 var icon = self.settings.icon, image; 28332 28333 image = self.settings.image; 28334 if (image) { 28335 icon = 'none'; 28336 28337 // Support for [high dpi, low dpi] image sources 28338 if (typeof image != "string") { 28339 image = window.getSelection ? image[0] : image[1]; 28340 } 28341 28342 image = ' style="background-image: url(\'' + image + '\')"'; 28343 } else { 28344 image = ''; 28345 } 28346 28347 icon = self.settings.icon ? prefix + 'ico ' + prefix + 'i-' + icon : ''; 28348 28349 return ( 28350 '<div id="' + id + '" class="' + self.classes() + '" tabindex="-1" aria-labelledby="' + id + '">' + 28351 '<button role="presentation" type="button" tabindex="-1">' + 28352 (icon ? '<i class="' + icon + '"' + image + '></i>' : '') + 28353 (self._text ? (icon ? '\u00a0' : '') + self.encode(self._text) : '') + 28354 '</button>' + 28355 '</div>' 28356 ); 28357 } 28358 }); 28359 }); 28360 28361 // Included from: js/tinymce/classes/ui/ButtonGroup.js 28362 28363 /** 28364 * ButtonGroup.js 28365 * 28366 * Copyright, Moxiecode Systems AB 28367 * Released under LGPL License. 28368 * 28369 * License: http://www.tinymce.com/license 28370 * Contributing: http://www.tinymce.com/contributing 28371 */ 28372 28373 /** 28374 * This control enables you to put multiple buttons into a group. This is 28375 * useful when you want to combine similar toolbar buttons into a group. 28376 * 28377 * @example 28378 * // Create and render a buttongroup with two buttons to the body element 28379 * tinymce.ui.Factory.create({ 28380 * type: 'buttongroup', 28381 * items: [ 28382 * {text: 'Button A'}, 28383 * {text: 'Button B'} 28384 * ] 28385 * }).renderTo(document.body); 28386 * 28387 * @-x-less ButtonGroup.less 28388 * @class tinymce.ui.ButtonGroup 28389 * @extends tinymce.ui.Container 28390 */ 28391 define("tinymce/ui/ButtonGroup", [ 28392 "tinymce/ui/Container" 28393 ], function(Container) { 28394 "use strict"; 28395 28396 return Container.extend({ 28397 Defaults: { 28398 defaultType: 'button', 28399 role: 'group' 28400 }, 28401 28402 /** 28403 * Renders the control as a HTML string. 28404 * 28405 * @method renderHtml 28406 * @return {String} HTML representing the control. 28407 */ 28408 renderHtml: function() { 28409 var self = this, layout = self._layout; 28410 28411 self.addClass('btn-group'); 28412 self.preRender(); 28413 layout.preRender(self); 28414 28415 return ( 28416 '<div id="' + self._id + '" class="' + self.classes() + '">' + 28417 '<div id="' + self._id + '-body">' + 28418 (self.settings.html || '') + layout.renderHtml(self) + 28419 '</div>' + 28420 '</div>' 28421 ); 28422 } 28423 }); 28424 }); 28425 28426 // Included from: js/tinymce/classes/ui/Checkbox.js 28427 28428 /** 28429 * Checkbox.js 28430 * 28431 * Copyright, Moxiecode Systems AB 28432 * Released under LGPL License. 28433 * 28434 * License: http://www.tinymce.com/license 28435 * Contributing: http://www.tinymce.com/contributing 28436 */ 28437 28438 /** 28439 * This control creates a custom checkbox. 28440 * 28441 * @example 28442 * // Create and render a checkbox to the body element 28443 * tinymce.ui.Factory.create({ 28444 * type: 'checkbox', 28445 * checked: true, 28446 * text: 'My checkbox' 28447 * }).renderTo(document.body); 28448 * 28449 * @-x-less Checkbox.less 28450 * @class tinymce.ui.Checkbox 28451 * @extends tinymce.ui.Widget 28452 */ 28453 define("tinymce/ui/Checkbox", [ 28454 "tinymce/ui/Widget" 28455 ], function(Widget) { 28456 "use strict"; 28457 28458 return Widget.extend({ 28459 Defaults: { 28460 classes: "checkbox", 28461 role: "checkbox", 28462 checked: false 28463 }, 28464 28465 /** 28466 * Constructs a new Checkbox instance with the specified settings. 28467 * 28468 * @constructor 28469 * @param {Object} settings Name/value object with settings. 28470 * @setting {Boolean} checked True if the checkbox should be checked by default. 28471 */ 28472 init: function(settings) { 28473 var self = this; 28474 28475 self._super(settings); 28476 28477 self.on('click mousedown', function(e) { 28478 e.preventDefault(); 28479 }); 28480 28481 self.on('click', function(e) { 28482 e.preventDefault(); 28483 28484 if (!self.disabled()) { 28485 self.checked(!self.checked()); 28486 } 28487 }); 28488 28489 self.checked(self.settings.checked); 28490 }, 28491 28492 /** 28493 * Getter/setter function for the checked state. 28494 * 28495 * @method checked 28496 * @param {Boolean} [state] State to be set. 28497 * @return {Boolean|tinymce.ui.Checkbox} True/false or checkbox if it's a set operation. 28498 */ 28499 checked: function(state) { 28500 var self = this; 28501 28502 if (typeof state != "undefined") { 28503 if (state) { 28504 self.addClass('checked'); 28505 } else { 28506 self.removeClass('checked'); 28507 } 28508 28509 self._checked = state; 28510 self.aria('checked', state); 28511 28512 return self; 28513 } 28514 28515 return self._checked; 28516 }, 28517 28518 /** 28519 * Getter/setter function for the value state. 28520 * 28521 * @method value 28522 * @param {Boolean} [state] State to be set. 28523 * @return {Boolean|tinymce.ui.Checkbox} True/false or checkbox if it's a set operation. 28524 */ 28525 value: function(state) { 28526 return this.checked(state); 28527 }, 28528 28529 /** 28530 * Renders the control as a HTML string. 28531 * 28532 * @method renderHtml 28533 * @return {String} HTML representing the control. 28534 */ 28535 renderHtml: function() { 28536 var self = this, id = self._id, prefix = self.classPrefix; 28537 28538 return ( 28539 '<div id="' + id + '" class="' + self.classes() + '" unselectable="on" aria-labelledby="' + id + '-al" tabindex="-1">' + 28540 '<i class="' + prefix + 'ico ' + prefix + 'i-checkbox"></i>' + 28541 '<span id="' + id + '-al" class="' + prefix + 'label">' + self.encode(self._text) + '</span>' + 28542 '</div>' 28543 ); 28544 } 28545 }); 28546 }); 28547 28548 // Included from: js/tinymce/classes/ui/PanelButton.js 28549 28550 /** 28551 * PanelButton.js 28552 * 28553 * Copyright, Moxiecode Systems AB 28554 * Released under LGPL License. 28555 * 28556 * License: http://www.tinymce.com/license 28557 * Contributing: http://www.tinymce.com/contributing 28558 */ 28559 28560 /** 28561 * Creates a new panel button. 28562 * 28563 * @class tinymce.ui.PanelButton 28564 * @extends tinymce.ui.Button 28565 */ 28566 define("tinymce/ui/PanelButton", [ 28567 "tinymce/ui/Button", 28568 "tinymce/ui/FloatPanel" 28569 ], function(Button, FloatPanel) { 28570 "use strict"; 28571 28572 return Button.extend({ 28573 /** 28574 * Shows the panel for the button. 28575 * 28576 * @method showPanel 28577 */ 28578 showPanel: function() { 28579 var self = this, settings = self.settings; 28580 28581 self.active(true); 28582 28583 if (!self.panel) { 28584 var panelSettings = settings.panel; 28585 28586 // Wrap panel in grid layout if type if specified 28587 // This makes it possible to add forms or other containers directly in the panel option 28588 if (panelSettings.type) { 28589 panelSettings = { 28590 layout: 'grid', 28591 items: panelSettings 28592 }; 28593 } 28594 28595 panelSettings.role = panelSettings.role || 'dialog'; 28596 panelSettings.popover = true; 28597 panelSettings.autohide = true; 28598 panelSettings.ariaRoot = true; 28599 28600 self.panel = new FloatPanel(panelSettings).on('hide', function() { 28601 self.active(false); 28602 }).on('cancel', function(e) { 28603 e.stopPropagation(); 28604 self.focus(); 28605 self.hidePanel(); 28606 }).parent(self).renderTo(self.getContainerElm()); 28607 28608 self.panel.fire('show'); 28609 self.panel.reflow(); 28610 } else { 28611 self.panel.show(); 28612 } 28613 28614 self.panel.moveRel(self.getEl(), settings.popoverAlign || (self.isRtl() ? ['bc-tr', 'bc-tc'] : ['bc-tl', 'bc-tc'])); 28615 }, 28616 28617 /** 28618 * Hides the panel for the button. 28619 * 28620 * @method hidePanel 28621 */ 28622 hidePanel: function() { 28623 var self = this; 28624 28625 if (self.panel) { 28626 self.panel.hide(); 28627 } 28628 }, 28629 28630 /** 28631 * Called after the control has been rendered. 28632 * 28633 * @method postRender 28634 */ 28635 postRender: function() { 28636 var self = this; 28637 28638 self.aria('haspopup', true); 28639 28640 self.on('click', function(e) { 28641 if (e.control === self) { 28642 if (self.panel && self.panel.visible()) { 28643 self.hidePanel(); 28644 } else { 28645 self.showPanel(); 28646 self.panel.focus(!!e.aria); 28647 } 28648 } 28649 }); 28650 28651 return self._super(); 28652 } 28653 }); 28654 }); 28655 28656 // Included from: js/tinymce/classes/ui/ColorButton.js 28657 28658 /** 28659 * ColorButton.js 28660 * 28661 * Copyright, Moxiecode Systems AB 28662 * Released under LGPL License. 28663 * 28664 * License: http://www.tinymce.com/license 28665 * Contributing: http://www.tinymce.com/contributing 28666 */ 28667 28668 /** 28669 * This class creates a color button control. This is a split button in which the main 28670 * button has a visual representation of the currently selected color. When clicked 28671 * the caret button displays a color picker, allowing the user to select a new color. 28672 * 28673 * @-x-less ColorButton.less 28674 * @class tinymce.ui.ColorButton 28675 * @extends tinymce.ui.PanelButton 28676 */ 28677 define("tinymce/ui/ColorButton", [ 28678 "tinymce/ui/PanelButton", 28679 "tinymce/dom/DOMUtils" 28680 ], function(PanelButton, DomUtils) { 28681 "use strict"; 28682 28683 var DOM = DomUtils.DOM; 28684 28685 return PanelButton.extend({ 28686 /** 28687 * Constructs a new ColorButton instance with the specified settings. 28688 * 28689 * @constructor 28690 * @param {Object} settings Name/value object with settings. 28691 */ 28692 init: function(settings) { 28693 this._super(settings); 28694 this.addClass('colorbutton'); 28695 }, 28696 28697 /** 28698 * Getter/setter for the current color. 28699 * 28700 * @method color 28701 * @param {String} [color] Color to set. 28702 * @return {String|tinymce.ui.ColorButton} Current color or current instance. 28703 */ 28704 color: function(color) { 28705 if (color) { 28706 this._color = color; 28707 this.getEl('preview').style.backgroundColor = color; 28708 return this; 28709 } 28710 28711 return this._color; 28712 }, 28713 28714 /** 28715 * Renders the control as a HTML string. 28716 * 28717 * @method renderHtml 28718 * @return {String} HTML representing the control. 28719 */ 28720 renderHtml: function() { 28721 var self = this, id = self._id, prefix = self.classPrefix; 28722 var icon = self.settings.icon ? prefix + 'ico ' + prefix + 'i-' + self.settings.icon : ''; 28723 var image = self.settings.image ? ' style="background-image: url(\'' + self.settings.image + '\')"' : ''; 28724 28725 return ( 28726 '<div id="' + id + '" class="' + self.classes() + '" role="button" tabindex="-1" aria-haspopup="true">' + 28727 '<button role="presentation" hidefocus="1" type="button" tabindex="-1">' + 28728 (icon ? '<i class="' + icon + '"' + image + '></i>' : '') + 28729 '<span id="' + id + '-preview" class="' + prefix + 'preview"></span>' + 28730 (self._text ? (icon ? ' ' : '') + (self._text) : '') + 28731 '</button>' + 28732 '<button type="button" class="' + prefix + 'open" hidefocus="1" tabindex="-1">' + 28733 ' <i class="' + prefix + 'caret"></i>' + 28734 '</button>' + 28735 '</div>' 28736 ); 28737 }, 28738 28739 /** 28740 * Called after the control has been rendered. 28741 * 28742 * @method postRender 28743 */ 28744 postRender: function() { 28745 var self = this, onClickHandler = self.settings.onclick; 28746 28747 self.on('click', function(e) { 28748 if (e.aria && e.aria.key == 'down') { 28749 return; 28750 } 28751 28752 if (e.control == self && !DOM.getParent(e.target, '.' + self.classPrefix + 'open')) { 28753 e.stopImmediatePropagation(); 28754 onClickHandler.call(self, e); 28755 } 28756 }); 28757 28758 delete self.settings.onclick; 28759 28760 return self._super(); 28761 } 28762 28763 }); 28764 }); 28765 28766 // Included from: js/tinymce/classes/ui/ComboBox.js 28767 28768 /** 28769 * ComboBox.js 28770 * 28771 * Copyright, Moxiecode Systems AB 28772 * Released under LGPL License. 28773 * 28774 * License: http://www.tinymce.com/license 28775 * Contributing: http://www.tinymce.com/contributing 28776 */ 28777 28778 /** 28779 * This class creates a combobox control. Select box that you select a value from or 28780 * type a value into. 28781 * 28782 * @-x-less ComboBox.less 28783 * @class tinymce.ui.ComboBox 28784 * @extends tinymce.ui.Widget 28785 */ 28786 define("tinymce/ui/ComboBox", [ 28787 "tinymce/ui/Widget", 28788 "tinymce/ui/Factory", 28789 "tinymce/ui/DomUtils" 28790 ], function(Widget, Factory, DomUtils) { 28791 "use strict"; 28792 28793 return Widget.extend({ 28794 /** 28795 * Constructs a new control instance with the specified settings. 28796 * 28797 * @constructor 28798 * @param {Object} settings Name/value object with settings. 28799 * @setting {String} placeholder Placeholder text to display. 28800 */ 28801 init: function(settings) { 28802 var self = this; 28803 28804 self._super(settings); 28805 self.addClass('combobox'); 28806 self.subinput = true; 28807 self.ariaTarget = 'inp'; // TODO: Figure out a better way 28808 28809 settings = self.settings; 28810 settings.menu = settings.menu || settings.values; 28811 28812 if (settings.menu) { 28813 settings.icon = 'caret'; 28814 } 28815 28816 self.on('click', function(e) { 28817 var elm = e.target, root = self.getEl(); 28818 28819 while (elm && elm != root) { 28820 if (elm.id && elm.id.indexOf('-open') != -1) { 28821 self.fire('action'); 28822 28823 if (settings.menu) { 28824 self.showMenu(); 28825 28826 if (e.aria) { 28827 self.menu.items()[0].focus(); 28828 } 28829 } 28830 } 28831 28832 elm = elm.parentNode; 28833 } 28834 }); 28835 28836 // TODO: Rework this 28837 self.on('keydown', function(e) { 28838 if (e.target.nodeName == "INPUT" && e.keyCode == 13) { 28839 self.parents().reverse().each(function(ctrl) { 28840 e.preventDefault(); 28841 self.fire('change'); 28842 28843 if (ctrl.hasEventListeners('submit') && ctrl.toJSON) { 28844 ctrl.fire('submit', {data: ctrl.toJSON()}); 28845 return false; 28846 } 28847 }); 28848 } 28849 }); 28850 28851 if (settings.placeholder) { 28852 self.addClass('placeholder'); 28853 28854 self.on('focusin', function() { 28855 if (!self._hasOnChange) { 28856 DomUtils.on(self.getEl('inp'), 'change', function() { 28857 self.fire('change'); 28858 }); 28859 28860 self._hasOnChange = true; 28861 } 28862 28863 if (self.hasClass('placeholder')) { 28864 self.getEl('inp').value = ''; 28865 self.removeClass('placeholder'); 28866 } 28867 }); 28868 28869 self.on('focusout', function() { 28870 if (self.value().length === 0) { 28871 self.getEl('inp').value = settings.placeholder; 28872 self.addClass('placeholder'); 28873 } 28874 }); 28875 } 28876 }, 28877 28878 showMenu: function() { 28879 var self = this, settings = self.settings, menu; 28880 28881 if (!self.menu) { 28882 menu = settings.menu || []; 28883 28884 // Is menu array then auto constuct menu control 28885 if (menu.length) { 28886 menu = { 28887 type: 'menu', 28888 items: menu 28889 }; 28890 } else { 28891 menu.type = menu.type || 'menu'; 28892 } 28893 28894 self.menu = Factory.create(menu).parent(self).renderTo(self.getContainerElm()); 28895 self.fire('createmenu'); 28896 self.menu.reflow(); 28897 self.menu.on('cancel', function(e) { 28898 if (e.control === self.menu) { 28899 self.focus(); 28900 } 28901 }); 28902 28903 self.menu.on('show hide', function(e) { 28904 e.control.items().each(function(ctrl) { 28905 ctrl.active(ctrl.value() == self.value()); 28906 }); 28907 }).fire('show'); 28908 28909 self.menu.on('select', function(e) { 28910 self.value(e.control.value()); 28911 }); 28912 28913 self.on('focusin', function(e) { 28914 if (e.target.tagName.toUpperCase() == 'INPUT') { 28915 self.menu.hide(); 28916 } 28917 }); 28918 28919 self.aria('expanded', true); 28920 } 28921 28922 self.menu.show(); 28923 self.menu.layoutRect({w: self.layoutRect().w}); 28924 self.menu.moveRel(self.getEl(), self.isRtl() ? ['br-tr', 'tr-br'] : ['bl-tl', 'tl-bl']); 28925 }, 28926 28927 /** 28928 * Getter/setter function for the control value. 28929 * 28930 * @method value 28931 * @param {String} [value] Value to be set. 28932 * @return {String|tinymce.ui.ComboBox} Value or self if it's a set operation. 28933 */ 28934 value: function(value) { 28935 var self = this; 28936 28937 if (typeof(value) != "undefined") { 28938 self._value = value; 28939 self.removeClass('placeholder'); 28940 28941 if (self._rendered) { 28942 self.getEl('inp').value = value; 28943 } 28944 28945 return self; 28946 } 28947 28948 if (self._rendered) { 28949 value = self.getEl('inp').value; 28950 28951 if (value != self.settings.placeholder) { 28952 return value; 28953 } 28954 28955 return ''; 28956 } 28957 28958 return self._value; 28959 }, 28960 28961 /** 28962 * Getter/setter function for the disabled state. 28963 * 28964 * @method value 28965 * @param {Boolean} [state] State to be set. 28966 * @return {Boolean|tinymce.ui.ComboBox} True/false or self if it's a set operation. 28967 */ 28968 disabled: function(state) { 28969 var self = this; 28970 28971 if (self._rendered && typeof(state) != 'undefined') { 28972 self.getEl('inp').disabled = state; 28973 } 28974 28975 return self._super(state); 28976 }, 28977 28978 /** 28979 * Focuses the input area of the control. 28980 * 28981 * @method focus 28982 */ 28983 focus: function() { 28984 this.getEl('inp').focus(); 28985 }, 28986 28987 /** 28988 * Repaints the control after a layout operation. 28989 * 28990 * @method repaint 28991 */ 28992 repaint: function() { 28993 var self = this, elm = self.getEl(), openElm = self.getEl('open'), rect = self.layoutRect(); 28994 var width, lineHeight; 28995 28996 if (openElm) { 28997 width = rect.w - DomUtils.getSize(openElm).width - 10; 28998 } else { 28999 width = rect.w - 10; 29000 } 29001 29002 // Detect old IE 7+8 add lineHeight to align caret vertically in the middle 29003 var doc = document; 29004 if (doc.all && (!doc.documentMode || doc.documentMode <= 8)) { 29005 lineHeight = (self.layoutRect().h - 2) + 'px'; 29006 } 29007 29008 DomUtils.css(elm.firstChild, { 29009 width: width, 29010 lineHeight: lineHeight 29011 }); 29012 29013 self._super(); 29014 29015 return self; 29016 }, 29017 29018 /** 29019 * Post render method. Called after the control has been rendered to the target. 29020 * 29021 * @method postRender 29022 * @return {tinymce.ui.ComboBox} Current combobox instance. 29023 */ 29024 postRender: function() { 29025 var self = this; 29026 29027 DomUtils.on(this.getEl('inp'), 'change', function() { 29028 self.fire('change'); 29029 }); 29030 29031 return self._super(); 29032 }, 29033 29034 remove: function() { 29035 DomUtils.off(this.getEl('inp')); 29036 this._super(); 29037 }, 29038 29039 /** 29040 * Renders the control as a HTML string. 29041 * 29042 * @method renderHtml 29043 * @return {String} HTML representing the control. 29044 */ 29045 renderHtml: function() { 29046 var self = this, id = self._id, settings = self.settings, prefix = self.classPrefix; 29047 var value = settings.value || settings.placeholder || ''; 29048 var icon, text, openBtnHtml = '', extraAttrs = ''; 29049 29050 if ("spellcheck" in settings) { 29051 extraAttrs += ' spellcheck="' + settings.spellcheck + '"'; 29052 } 29053 29054 if (settings.maxLength) { 29055 extraAttrs += ' maxlength="' + settings.maxLength + '"'; 29056 } 29057 29058 if (settings.size) { 29059 extraAttrs += ' size="' + settings.size + '"'; 29060 } 29061 29062 if (settings.subtype) { 29063 extraAttrs += ' type="' + settings.subtype + '"'; 29064 } 29065 29066 if (self.disabled()) { 29067 extraAttrs += ' disabled="disabled"'; 29068 } 29069 29070 icon = settings.icon; 29071 if (icon && icon != 'caret') { 29072 icon = prefix + 'ico ' + prefix + 'i-' + settings.icon; 29073 } 29074 29075 text = self._text; 29076 29077 if (icon || text) { 29078 openBtnHtml = ( 29079 '<div id="' + id + '-open" class="' + prefix + 'btn ' + prefix + 'open" tabIndex="-1" role="button">' + 29080 '<button id="' + id + '-action" type="button" hidefocus="1" tabindex="-1">' + 29081 (icon != 'caret' ? '<i class="' + icon + '"></i>' : '<i class="' + prefix + 'caret"></i>') + 29082 (text ? (icon ? ' ' : '') + text : '') + 29083 '</button>' + 29084 '</div>' 29085 ); 29086 29087 self.addClass('has-open'); 29088 } 29089 29090 return ( 29091 '<div id="' + id + '" class="' + self.classes() + '">' + 29092 '<input id="' + id + '-inp" class="' + prefix + 'textbox ' + prefix + 'placeholder" value="' + 29093 value + '" hidefocus="1"' + extraAttrs + ' />' + 29094 openBtnHtml + 29095 '</div>' 29096 ); 29097 } 29098 }); 29099 }); 29100 29101 // Included from: js/tinymce/classes/ui/Path.js 29102 29103 /** 29104 * Path.js 29105 * 29106 * Copyright, Moxiecode Systems AB 29107 * Released under LGPL License. 29108 * 29109 * License: http://www.tinymce.com/license 29110 * Contributing: http://www.tinymce.com/contributing 29111 */ 29112 29113 /** 29114 * Creates a new path control. 29115 * 29116 * @-x-less Path.less 29117 * @class tinymce.ui.Path 29118 * @extends tinymce.ui.Widget 29119 */ 29120 define("tinymce/ui/Path", [ 29121 "tinymce/ui/Widget" 29122 ], function(Widget) { 29123 "use strict"; 29124 29125 return Widget.extend({ 29126 /** 29127 * Constructs a instance with the specified settings. 29128 * 29129 * @constructor 29130 * @param {Object} settings Name/value object with settings. 29131 * @setting {String} delimiter Delimiter to display between items in path. 29132 */ 29133 init: function(settings) { 29134 var self = this; 29135 29136 if (!settings.delimiter) { 29137 settings.delimiter = '\u00BB'; 29138 } 29139 29140 self._super(settings); 29141 self.addClass('path'); 29142 self.canFocus = true; 29143 29144 self.on('click', function(e) { 29145 var index, target = e.target; 29146 29147 if ((index = target.getAttribute('data-index'))) { 29148 self.fire('select', {value: self.data()[index], index: index}); 29149 } 29150 }); 29151 }, 29152 29153 /** 29154 * Focuses the current control. 29155 * 29156 * @method focus 29157 * @return {tinymce.ui.Control} Current control instance. 29158 */ 29159 focus: function() { 29160 var self = this; 29161 29162 self.getEl().firstChild.focus(); 29163 29164 return self; 29165 }, 29166 29167 /** 29168 * Sets/gets the data to be used for the path. 29169 * 29170 * @method data 29171 * @param {Array} data Array with items name is rendered to path. 29172 */ 29173 data: function(data) { 29174 var self = this; 29175 29176 if (typeof(data) !== "undefined") { 29177 self._data = data; 29178 self.update(); 29179 29180 return self; 29181 } 29182 29183 return self._data; 29184 }, 29185 29186 /** 29187 * Updated the path. 29188 * 29189 * @private 29190 */ 29191 update: function() { 29192 this.innerHtml(this._getPathHtml()); 29193 }, 29194 29195 /** 29196 * Called after the control has been rendered. 29197 * 29198 * @method postRender 29199 */ 29200 postRender: function() { 29201 var self = this; 29202 29203 self._super(); 29204 29205 self.data(self.settings.data); 29206 }, 29207 29208 /** 29209 * Renders the control as a HTML string. 29210 * 29211 * @method renderHtml 29212 * @return {String} HTML representing the control. 29213 */ 29214 renderHtml: function() { 29215 var self = this; 29216 29217 return ( 29218 '<div id="' + self._id + '" class="' + self.classes() + '">' + 29219 self._getPathHtml() + 29220 '</div>' 29221 ); 29222 }, 29223 29224 _getPathHtml: function() { 29225 var self = this, parts = self._data || [], i, l, html = '', prefix = self.classPrefix; 29226 29227 for (i = 0, l = parts.length; i < l; i++) { 29228 html += ( 29229 (i > 0 ? '<div class="' + prefix + 'divider" aria-hidden="true"> ' + self.settings.delimiter + ' </div>' : '') + 29230 '<div role="button" class="' + prefix + 'path-item' + (i == l - 1 ? ' ' + prefix + 'last' : '') + '" data-index="' + 29231 i + '" tabindex="-1" id="' + self._id + '-' + i + '" aria-level="' + i + '">' + parts[i].name + '</div>' 29232 ); 29233 } 29234 29235 if (!html) { 29236 html = '<div class="' + prefix + 'path-item">\u00a0</div>'; 29237 } 29238 29239 return html; 29240 } 29241 }); 29242 }); 29243 29244 // Included from: js/tinymce/classes/ui/ElementPath.js 29245 29246 /** 29247 * ElementPath.js 29248 * 29249 * Copyright, Moxiecode Systems AB 29250 * Released under LGPL License. 29251 * 29252 * License: http://www.tinymce.com/license 29253 * Contributing: http://www.tinymce.com/contributing 29254 */ 29255 29256 /** 29257 * This control creates an path for the current selections parent elements in TinyMCE. 29258 * 29259 * @class tinymce.ui.ElementPath 29260 * @extends tinymce.ui.Path 29261 */ 29262 define("tinymce/ui/ElementPath", [ 29263 "tinymce/ui/Path", 29264 "tinymce/EditorManager" 29265 ], function(Path, EditorManager) { 29266 return Path.extend({ 29267 /** 29268 * Post render method. Called after the control has been rendered to the target. 29269 * 29270 * @method postRender 29271 * @return {tinymce.ui.ElementPath} Current combobox instance. 29272 */ 29273 postRender: function() { 29274 var self = this, editor = EditorManager.activeEditor; 29275 29276 function isHidden(elm) { 29277 if (elm.nodeType === 1) { 29278 if (elm.nodeName == "BR" || !!elm.getAttribute('data-mce-bogus')) { 29279 return true; 29280 } 29281 29282 if (elm.getAttribute('data-mce-type') === 'bookmark') { 29283 return true; 29284 } 29285 } 29286 29287 return false; 29288 } 29289 29290 self.on('select', function(e) { 29291 var parents = [], node, body = editor.getBody(); 29292 29293 editor.focus(); 29294 29295 node = editor.selection.getStart(); 29296 while (node && node != body) { 29297 if (!isHidden(node)) { 29298 parents.push(node); 29299 } 29300 29301 node = node.parentNode; 29302 } 29303 29304 editor.selection.select(parents[parents.length - 1 - e.index]); 29305 editor.nodeChanged(); 29306 }); 29307 29308 editor.on('nodeChange', function(e) { 29309 var parents = [], selectionParents = e.parents, i = selectionParents.length; 29310 29311 while (i--) { 29312 if (selectionParents[i].nodeType == 1 && !isHidden(selectionParents[i])) { 29313 var args = editor.fire('ResolveName', { 29314 name: selectionParents[i].nodeName.toLowerCase(), 29315 target: selectionParents[i] 29316 }); 29317 29318 parents.push({name: args.name}); 29319 } 29320 } 29321 29322 self.data(parents); 29323 }); 29324 29325 return self._super(); 29326 } 29327 }); 29328 }); 29329 29330 // Included from: js/tinymce/classes/ui/FormItem.js 29331 29332 /** 29333 * FormItem.js 29334 * 29335 * Copyright, Moxiecode Systems AB 29336 * Released under LGPL License. 29337 * 29338 * License: http://www.tinymce.com/license 29339 * Contributing: http://www.tinymce.com/contributing 29340 */ 29341 29342 /** 29343 * This class is a container created by the form element with 29344 * a label and control item. 29345 * 29346 * @class tinymce.ui.FormItem 29347 * @extends tinymce.ui.Container 29348 * @setting {String} label Label to display for the form item. 29349 */ 29350 define("tinymce/ui/FormItem", [ 29351 "tinymce/ui/Container" 29352 ], function(Container) { 29353 "use strict"; 29354 29355 return Container.extend({ 29356 Defaults: { 29357 layout: 'flex', 29358 align: 'center', 29359 defaults: { 29360 flex: 1 29361 } 29362 }, 29363 29364 /** 29365 * Renders the control as a HTML string. 29366 * 29367 * @method renderHtml 29368 * @return {String} HTML representing the control. 29369 */ 29370 renderHtml: function() { 29371 var self = this, layout = self._layout, prefix = self.classPrefix; 29372 29373 self.addClass('formitem'); 29374 layout.preRender(self); 29375 29376 return ( 29377 '<div id="' + self._id + '" class="' + self.classes() + '" hidefocus="1" tabindex="-1">' + 29378 (self.settings.title ? ('<div id="' + self._id + '-title" class="' + prefix + 'title">' + 29379 self.settings.title + '</div>') : '') + 29380 '<div id="' + self._id + '-body" class="' + self.classes('body') + '">' + 29381 (self.settings.html || '') + layout.renderHtml(self) + 29382 '</div>' + 29383 '</div>' 29384 ); 29385 } 29386 }); 29387 }); 29388 29389 // Included from: js/tinymce/classes/ui/Form.js 29390 29391 /** 29392 * Form.js 29393 * 29394 * Copyright, Moxiecode Systems AB 29395 * Released under LGPL License. 29396 * 29397 * License: http://www.tinymce.com/license 29398 * Contributing: http://www.tinymce.com/contributing 29399 */ 29400 29401 /** 29402 * This class creates a form container. A form container has the ability 29403 * to automatically wrap items in tinymce.ui.FormItem instances. 29404 * 29405 * Each FormItem instance is a container for the label and the item. 29406 * 29407 * @example 29408 * tinymce.ui.Factory.create({ 29409 * type: 'form', 29410 * items: [ 29411 * {type: 'textbox', label: 'My text box'} 29412 * ] 29413 * }).renderTo(document.body); 29414 * 29415 * @class tinymce.ui.Form 29416 * @extends tinymce.ui.Container 29417 */ 29418 define("tinymce/ui/Form", [ 29419 "tinymce/ui/Container", 29420 "tinymce/ui/FormItem" 29421 ], function(Container, FormItem) { 29422 "use strict"; 29423 29424 return Container.extend({ 29425 Defaults: { 29426 containerCls: 'form', 29427 layout: 'flex', 29428 direction: 'column', 29429 align: 'stretch', 29430 flex: 1, 29431 padding: 20, 29432 labelGap: 30, 29433 spacing: 10, 29434 callbacks: { 29435 submit: function() { 29436 this.submit(); 29437 } 29438 } 29439 }, 29440 29441 /** 29442 * This method gets invoked before the control is rendered. 29443 * 29444 * @method preRender 29445 */ 29446 preRender: function() { 29447 var self = this, items = self.items(); 29448 29449 // Wrap any labeled items in FormItems 29450 items.each(function(ctrl) { 29451 var formItem, label = ctrl.settings.label; 29452 29453 if (label) { 29454 formItem = new FormItem({ 29455 layout: 'flex', 29456 autoResize: "overflow", 29457 defaults: {flex: 1}, 29458 items: [ 29459 {type: 'label', id: ctrl._id + '-l', text: label, flex: 0, forId: ctrl._id, disabled: ctrl.disabled()} 29460 ] 29461 }); 29462 29463 formItem.type = 'formitem'; 29464 ctrl.aria('labelledby', ctrl._id + '-l'); 29465 29466 if (typeof(ctrl.settings.flex) == "undefined") { 29467 ctrl.settings.flex = 1; 29468 } 29469 29470 self.replace(ctrl, formItem); 29471 formItem.add(ctrl); 29472 } 29473 }); 29474 }, 29475 29476 /** 29477 * Recalcs label widths. 29478 * 29479 * @private 29480 */ 29481 recalcLabels: function() { 29482 var self = this, maxLabelWidth = 0, labels = [], i, labelGap; 29483 29484 if (self.settings.labelGapCalc === false) { 29485 return; 29486 } 29487 29488 self.items().filter('formitem').each(function(item) { 29489 var labelCtrl = item.items()[0], labelWidth = labelCtrl.getEl().clientWidth; 29490 29491 maxLabelWidth = labelWidth > maxLabelWidth ? labelWidth : maxLabelWidth; 29492 labels.push(labelCtrl); 29493 }); 29494 29495 labelGap = self.settings.labelGap || 0; 29496 29497 i = labels.length; 29498 while (i--) { 29499 labels[i].settings.minWidth = maxLabelWidth + labelGap; 29500 } 29501 }, 29502 29503 /** 29504 * Getter/setter for the visibility state. 29505 * 29506 * @method visible 29507 * @param {Boolean} [state] True/false state to show/hide. 29508 * @return {tinymce.ui.Form|Boolean} True/false state or current control. 29509 */ 29510 visible: function(state) { 29511 var val = this._super(state); 29512 29513 if (state === true && this._rendered) { 29514 this.recalcLabels(); 29515 } 29516 29517 return val; 29518 }, 29519 29520 /** 29521 * Fires a submit event with the serialized form. 29522 * 29523 * @method submit 29524 * @return {Object} Event arguments object. 29525 */ 29526 submit: function() { 29527 return this.fire('submit', {data: this.toJSON()}); 29528 }, 29529 29530 /** 29531 * Post render method. Called after the control has been rendered to the target. 29532 * 29533 * @method postRender 29534 * @return {tinymce.ui.ComboBox} Current combobox instance. 29535 */ 29536 postRender: function() { 29537 var self = this; 29538 29539 self._super(); 29540 self.recalcLabels(); 29541 self.fromJSON(self.settings.data); 29542 } 29543 }); 29544 }); 29545 29546 // Included from: js/tinymce/classes/ui/FieldSet.js 29547 29548 /** 29549 * FieldSet.js 29550 * 29551 * Copyright, Moxiecode Systems AB 29552 * Released under LGPL License. 29553 * 29554 * License: http://www.tinymce.com/license 29555 * Contributing: http://www.tinymce.com/contributing 29556 */ 29557 29558 /** 29559 * This class creates fieldset containers. 29560 * 29561 * @-x-less FieldSet.less 29562 * @class tinymce.ui.FieldSet 29563 * @extends tinymce.ui.Form 29564 */ 29565 define("tinymce/ui/FieldSet", [ 29566 "tinymce/ui/Form" 29567 ], function(Form) { 29568 "use strict"; 29569 29570 return Form.extend({ 29571 Defaults: { 29572 containerCls: 'fieldset', 29573 layout: 'flex', 29574 direction: 'column', 29575 align: 'stretch', 29576 flex: 1, 29577 padding: "25 15 5 15", 29578 labelGap: 30, 29579 spacing: 10, 29580 border: 1 29581 }, 29582 29583 /** 29584 * Renders the control as a HTML string. 29585 * 29586 * @method renderHtml 29587 * @return {String} HTML representing the control. 29588 */ 29589 renderHtml: function() { 29590 var self = this, layout = self._layout, prefix = self.classPrefix; 29591 29592 self.preRender(); 29593 layout.preRender(self); 29594 29595 return ( 29596 '<fieldset id="' + self._id + '" class="' + self.classes() + '" hidefocus="1" tabindex="-1">' + 29597 (self.settings.title ? ('<legend id="' + self._id + '-title" class="' + prefix + 'fieldset-title">' + 29598 self.settings.title + '</legend>') : '') + 29599 '<div id="' + self._id + '-body" class="' + self.classes('body') + '">' + 29600 (self.settings.html || '') + layout.renderHtml(self) + 29601 '</div>' + 29602 '</fieldset>' 29603 ); 29604 } 29605 }); 29606 }); 29607 29608 // Included from: js/tinymce/classes/ui/FilePicker.js 29609 29610 /** 29611 * FilePicker.js 29612 * 29613 * Copyright, Moxiecode Systems AB 29614 * Released under LGPL License. 29615 * 29616 * License: http://www.tinymce.com/license 29617 * Contributing: http://www.tinymce.com/contributing 29618 */ 29619 29620 /*global tinymce:true */ 29621 29622 /** 29623 * This class creates a file picker control. 29624 * 29625 * @class tinymce.ui.FilePicker 29626 * @extends tinymce.ui.ComboBox 29627 */ 29628 define("tinymce/ui/FilePicker", [ 29629 "tinymce/ui/ComboBox" 29630 ], function(ComboBox) { 29631 "use strict"; 29632 29633 return ComboBox.extend({ 29634 /** 29635 * Constructs a new control instance with the specified settings. 29636 * 29637 * @constructor 29638 * @param {Object} settings Name/value object with settings. 29639 */ 29640 init: function(settings) { 29641 var self = this, editor = tinymce.activeEditor, fileBrowserCallback; 29642 29643 settings.spellcheck = false; 29644 29645 fileBrowserCallback = editor.settings.file_browser_callback; 29646 if (fileBrowserCallback) { 29647 settings.icon = 'browse'; 29648 29649 settings.onaction = function() { 29650 fileBrowserCallback( 29651 self.getEl('inp').id, 29652 self.getEl('inp').value, 29653 settings.filetype, 29654 window 29655 ); 29656 }; 29657 } 29658 29659 self._super(settings); 29660 } 29661 }); 29662 }); 29663 29664 // Included from: js/tinymce/classes/ui/FitLayout.js 29665 29666 /** 29667 * FitLayout.js 29668 * 29669 * Copyright, Moxiecode Systems AB 29670 * Released under LGPL License. 29671 * 29672 * License: http://www.tinymce.com/license 29673 * Contributing: http://www.tinymce.com/contributing 29674 */ 29675 29676 /** 29677 * This layout manager will resize the control to be the size of it's parent container. 29678 * In other words width: 100% and height: 100%. 29679 * 29680 * @-x-less FitLayout.less 29681 * @class tinymce.ui.FitLayout 29682 * @extends tinymce.ui.AbsoluteLayout 29683 */ 29684 define("tinymce/ui/FitLayout", [ 29685 "tinymce/ui/AbsoluteLayout" 29686 ], function(AbsoluteLayout) { 29687 "use strict"; 29688 29689 return AbsoluteLayout.extend({ 29690 /** 29691 * Recalculates the positions of the controls in the specified container. 29692 * 29693 * @method recalc 29694 * @param {tinymce.ui.Container} container Container instance to recalc. 29695 */ 29696 recalc: function(container) { 29697 var contLayoutRect = container.layoutRect(), paddingBox = container.paddingBox(); 29698 29699 container.items().filter(':visible').each(function(ctrl) { 29700 ctrl.layoutRect({ 29701 x: paddingBox.left, 29702 y: paddingBox.top, 29703 w: contLayoutRect.innerW - paddingBox.right - paddingBox.left, 29704 h: contLayoutRect.innerH - paddingBox.top - paddingBox.bottom 29705 }); 29706 29707 if (ctrl.recalc) { 29708 ctrl.recalc(); 29709 } 29710 }); 29711 } 29712 }); 29713 }); 29714 29715 // Included from: js/tinymce/classes/ui/FlexLayout.js 29716 29717 /** 29718 * FlexLayout.js 29719 * 29720 * Copyright, Moxiecode Systems AB 29721 * Released under LGPL License. 29722 * 29723 * License: http://www.tinymce.com/license 29724 * Contributing: http://www.tinymce.com/contributing 29725 */ 29726 29727 /** 29728 * This layout manager works similar to the CSS flex box. 29729 * 29730 * @setting {String} direction row|row-reverse|column|column-reverse 29731 * @setting {Number} flex A positive-number to flex by. 29732 * @setting {String} align start|end|center|stretch 29733 * @setting {String} pack start|end|justify 29734 * 29735 * @class tinymce.ui.FlexLayout 29736 * @extends tinymce.ui.AbsoluteLayout 29737 */ 29738 define("tinymce/ui/FlexLayout", [ 29739 "tinymce/ui/AbsoluteLayout" 29740 ], function(AbsoluteLayout) { 29741 "use strict"; 29742 29743 return AbsoluteLayout.extend({ 29744 /** 29745 * Recalculates the positions of the controls in the specified container. 29746 * 29747 * @method recalc 29748 * @param {tinymce.ui.Container} container Container instance to recalc. 29749 */ 29750 recalc: function(container) { 29751 // A ton of variables, needs to be in the same scope for performance 29752 var i, l, items, contLayoutRect, contPaddingBox, contSettings, align, pack, spacing, totalFlex, availableSpace, direction; 29753 var ctrl, ctrlLayoutRect, ctrlSettings, flex, maxSizeItems = [], size, maxSize, ratio, rect, pos, maxAlignEndPos; 29754 var sizeName, minSizeName, posName, maxSizeName, beforeName, innerSizeName, deltaSizeName, contentSizeName; 29755 var alignAxisName, alignInnerSizeName, alignSizeName, alignMinSizeName, alignBeforeName, alignAfterName; 29756 var alignDeltaSizeName, alignContentSizeName; 29757 var max = Math.max, min = Math.min; 29758 29759 // Get container items, properties and settings 29760 items = container.items().filter(':visible'); 29761 contLayoutRect = container.layoutRect(); 29762 contPaddingBox = container._paddingBox; 29763 contSettings = container.settings; 29764 direction = container.isRtl() ? (contSettings.direction || 'row-reversed') : contSettings.direction; 29765 align = contSettings.align; 29766 pack = container.isRtl() ? (contSettings.pack || 'end') : contSettings.pack; 29767 spacing = contSettings.spacing || 0; 29768 29769 if (direction == "row-reversed" || direction == "column-reverse") { 29770 items = items.set(items.toArray().reverse()); 29771 direction = direction.split('-')[0]; 29772 } 29773 29774 // Setup axis variable name for row/column direction since the calculations is the same 29775 if (direction == "column") { 29776 posName = "y"; 29777 sizeName = "h"; 29778 minSizeName = "minH"; 29779 maxSizeName = "maxH"; 29780 innerSizeName = "innerH"; 29781 beforeName = 'top'; 29782 deltaSizeName = "deltaH"; 29783 contentSizeName = "contentH"; 29784 29785 alignBeforeName = "left"; 29786 alignSizeName = "w"; 29787 alignAxisName = "x"; 29788 alignInnerSizeName = "innerW"; 29789 alignMinSizeName = "minW"; 29790 alignAfterName = "right"; 29791 alignDeltaSizeName = "deltaW"; 29792 alignContentSizeName = "contentW"; 29793 } else { 29794 posName = "x"; 29795 sizeName = "w"; 29796 minSizeName = "minW"; 29797 maxSizeName = "maxW"; 29798 innerSizeName = "innerW"; 29799 beforeName = 'left'; 29800 deltaSizeName = "deltaW"; 29801 contentSizeName = "contentW"; 29802 29803 alignBeforeName = "top"; 29804 alignSizeName = "h"; 29805 alignAxisName = "y"; 29806 alignInnerSizeName = "innerH"; 29807 alignMinSizeName = "minH"; 29808 alignAfterName = "bottom"; 29809 alignDeltaSizeName = "deltaH"; 29810 alignContentSizeName = "contentH"; 29811 } 29812 29813 // Figure out total flex, availableSpace and collect any max size elements 29814 availableSpace = contLayoutRect[innerSizeName] - contPaddingBox[beforeName] - contPaddingBox[beforeName]; 29815 maxAlignEndPos = totalFlex = 0; 29816 for (i = 0, l = items.length; i < l; i++) { 29817 ctrl = items[i]; 29818 ctrlLayoutRect = ctrl.layoutRect(); 29819 ctrlSettings = ctrl.settings; 29820 flex = ctrlSettings.flex; 29821 availableSpace -= (i < l - 1 ? spacing : 0); 29822 29823 if (flex > 0) { 29824 totalFlex += flex; 29825 29826 // Flexed item has a max size then we need to check if we will hit that size 29827 if (ctrlLayoutRect[maxSizeName]) { 29828 maxSizeItems.push(ctrl); 29829 } 29830 29831 ctrlLayoutRect.flex = flex; 29832 } 29833 29834 availableSpace -= ctrlLayoutRect[minSizeName]; 29835 29836 // Calculate the align end position to be used to check for overflow/underflow 29837 size = contPaddingBox[alignBeforeName] + ctrlLayoutRect[alignMinSizeName] + contPaddingBox[alignAfterName]; 29838 if (size > maxAlignEndPos) { 29839 maxAlignEndPos = size; 29840 } 29841 } 29842 29843 // Calculate minW/minH 29844 rect = {}; 29845 if (availableSpace < 0) { 29846 rect[minSizeName] = contLayoutRect[minSizeName] - availableSpace + contLayoutRect[deltaSizeName]; 29847 } else { 29848 rect[minSizeName] = contLayoutRect[innerSizeName] - availableSpace + contLayoutRect[deltaSizeName]; 29849 } 29850 29851 rect[alignMinSizeName] = maxAlignEndPos + contLayoutRect[alignDeltaSizeName]; 29852 29853 rect[contentSizeName] = contLayoutRect[innerSizeName] - availableSpace; 29854 rect[alignContentSizeName] = maxAlignEndPos; 29855 rect.minW = min(rect.minW, contLayoutRect.maxW); 29856 rect.minH = min(rect.minH, contLayoutRect.maxH); 29857 rect.minW = max(rect.minW, contLayoutRect.startMinWidth); 29858 rect.minH = max(rect.minH, contLayoutRect.startMinHeight); 29859 29860 // Resize container container if minSize was changed 29861 if (contLayoutRect.autoResize && (rect.minW != contLayoutRect.minW || rect.minH != contLayoutRect.minH)) { 29862 rect.w = rect.minW; 29863 rect.h = rect.minH; 29864 29865 container.layoutRect(rect); 29866 this.recalc(container); 29867 29868 // Forced recalc for example if items are hidden/shown 29869 if (container._lastRect === null) { 29870 var parentCtrl = container.parent(); 29871 if (parentCtrl) { 29872 parentCtrl._lastRect = null; 29873 parentCtrl.recalc(); 29874 } 29875 } 29876 29877 return; 29878 } 29879 29880 // Handle max size elements, check if they will become to wide with current options 29881 ratio = availableSpace / totalFlex; 29882 for (i = 0, l = maxSizeItems.length; i < l; i++) { 29883 ctrl = maxSizeItems[i]; 29884 ctrlLayoutRect = ctrl.layoutRect(); 29885 maxSize = ctrlLayoutRect[maxSizeName]; 29886 size = ctrlLayoutRect[minSizeName] + ctrlLayoutRect.flex * ratio; 29887 29888 if (size > maxSize) { 29889 availableSpace -= (ctrlLayoutRect[maxSizeName] - ctrlLayoutRect[minSizeName]); 29890 totalFlex -= ctrlLayoutRect.flex; 29891 ctrlLayoutRect.flex = 0; 29892 ctrlLayoutRect.maxFlexSize = maxSize; 29893 } else { 29894 ctrlLayoutRect.maxFlexSize = 0; 29895 } 29896 } 29897 29898 // Setup new ratio, target layout rect, start position 29899 ratio = availableSpace / totalFlex; 29900 pos = contPaddingBox[beforeName]; 29901 rect = {}; 29902 29903 // Handle pack setting moves the start position to end, center 29904 if (totalFlex === 0) { 29905 if (pack == "end") { 29906 pos = availableSpace + contPaddingBox[beforeName]; 29907 } else if (pack == "center") { 29908 pos = Math.round( 29909 (contLayoutRect[innerSizeName] / 2) - ((contLayoutRect[innerSizeName] - availableSpace) / 2) 29910 ) + contPaddingBox[beforeName]; 29911 29912 if (pos < 0) { 29913 pos = contPaddingBox[beforeName]; 29914 } 29915 } else if (pack == "justify") { 29916 pos = contPaddingBox[beforeName]; 29917 spacing = Math.floor(availableSpace / (items.length - 1)); 29918 } 29919 } 29920 29921 // Default aligning (start) the other ones needs to be calculated while doing the layout 29922 rect[alignAxisName] = contPaddingBox[alignBeforeName]; 29923 29924 // Start laying out controls 29925 for (i = 0, l = items.length; i < l; i++) { 29926 ctrl = items[i]; 29927 ctrlLayoutRect = ctrl.layoutRect(); 29928 size = ctrlLayoutRect.maxFlexSize || ctrlLayoutRect[minSizeName]; 29929 29930 // Align the control on the other axis 29931 if (align === "center") { 29932 rect[alignAxisName] = Math.round((contLayoutRect[alignInnerSizeName] / 2) - (ctrlLayoutRect[alignSizeName] / 2)); 29933 } else if (align === "stretch") { 29934 rect[alignSizeName] = max( 29935 ctrlLayoutRect[alignMinSizeName] || 0, 29936 contLayoutRect[alignInnerSizeName] - contPaddingBox[alignBeforeName] - contPaddingBox[alignAfterName] 29937 ); 29938 rect[alignAxisName] = contPaddingBox[alignBeforeName]; 29939 } else if (align === "end") { 29940 rect[alignAxisName] = contLayoutRect[alignInnerSizeName] - ctrlLayoutRect[alignSizeName] - contPaddingBox.top; 29941 } 29942 29943 // Calculate new size based on flex 29944 if (ctrlLayoutRect.flex > 0) { 29945 size += ctrlLayoutRect.flex * ratio; 29946 } 29947 29948 rect[sizeName] = size; 29949 rect[posName] = pos; 29950 ctrl.layoutRect(rect); 29951 29952 // Recalculate containers 29953 if (ctrl.recalc) { 29954 ctrl.recalc(); 29955 } 29956 29957 // Move x/y position 29958 pos += size + spacing; 29959 } 29960 } 29961 }); 29962 }); 29963 29964 // Included from: js/tinymce/classes/ui/FlowLayout.js 29965 29966 /** 29967 * FlowLayout.js 29968 * 29969 * Copyright, Moxiecode Systems AB 29970 * Released under LGPL License. 29971 * 29972 * License: http://www.tinymce.com/license 29973 * Contributing: http://www.tinymce.com/contributing 29974 */ 29975 29976 /** 29977 * This layout manager will place the controls by using the browsers native layout. 29978 * 29979 * @-x-less FlowLayout.less 29980 * @class tinymce.ui.FlowLayout 29981 * @extends tinymce.ui.Layout 29982 */ 29983 define("tinymce/ui/FlowLayout", [ 29984 "tinymce/ui/Layout" 29985 ], function(Layout) { 29986 return Layout.extend({ 29987 Defaults: { 29988 containerClass: 'flow-layout', 29989 controlClass: 'flow-layout-item', 29990 endClass : 'break' 29991 }, 29992 29993 /** 29994 * Recalculates the positions of the controls in the specified container. 29995 * 29996 * @method recalc 29997 * @param {tinymce.ui.Container} container Container instance to recalc. 29998 */ 29999 recalc: function(container) { 30000 container.items().filter(':visible').each(function(ctrl) { 30001 if (ctrl.recalc) { 30002 ctrl.recalc(); 30003 } 30004 }); 30005 } 30006 }); 30007 }); 30008 30009 // Included from: js/tinymce/classes/ui/FormatControls.js 30010 30011 /** 30012 * FormatControls.js 30013 * 30014 * Copyright, Moxiecode Systems AB 30015 * Released under LGPL License. 30016 * 30017 * License: http://www.tinymce.com/license 30018 * Contributing: http://www.tinymce.com/contributing 30019 */ 30020 30021 /** 30022 * Internal class containing all TinyMCE specific control types such as 30023 * format listboxes, fontlist boxes, toolbar buttons etc. 30024 * 30025 * @class tinymce.ui.FormatControls 30026 */ 30027 define("tinymce/ui/FormatControls", [ 30028 "tinymce/ui/Control", 30029 "tinymce/ui/Widget", 30030 "tinymce/ui/FloatPanel", 30031 "tinymce/util/Tools", 30032 "tinymce/EditorManager", 30033 "tinymce/Env" 30034 ], function(Control, Widget, FloatPanel, Tools, EditorManager, Env) { 30035 var each = Tools.each; 30036 30037 EditorManager.on('AddEditor', function(e) { 30038 if (e.editor.rtl) { 30039 Control.rtl = true; 30040 } 30041 30042 registerControls(e.editor); 30043 }); 30044 30045 Control.translate = function(text) { 30046 return EditorManager.translate(text); 30047 }; 30048 30049 Widget.tooltips = !Env.iOS; 30050 30051 function registerControls(editor) { 30052 var formatMenu; 30053 30054 function createListBoxChangeHandler(items, formatName) { 30055 return function() { 30056 var self = this; 30057 30058 editor.on('nodeChange', function(e) { 30059 var formatter = editor.formatter; 30060 var value = null; 30061 30062 each(e.parents, function(node) { 30063 each(items, function(item) { 30064 if (formatName) { 30065 if (formatter.matchNode(node, formatName, {value: item.value})) { 30066 value = item.value; 30067 } 30068 } else { 30069 if (formatter.matchNode(node, item.value)) { 30070 value = item.value; 30071 } 30072 } 30073 30074 if (value) { 30075 return false; 30076 } 30077 }); 30078 30079 if (value) { 30080 return false; 30081 } 30082 }); 30083 30084 self.value(value); 30085 }); 30086 }; 30087 } 30088 30089 function createFormats(formats) { 30090 formats = formats.replace(/;$/, '').split(';'); 30091 30092 var i = formats.length; 30093 while (i--) { 30094 formats[i] = formats[i].split('='); 30095 } 30096 30097 return formats; 30098 } 30099 30100 function createFormatMenu() { 30101 var count = 0, newFormats = []; 30102 30103 var defaultStyleFormats = [ 30104 {title: 'Headings', items: [ 30105 {title: 'Heading 1', format: 'h1'}, 30106 {title: 'Heading 2', format: 'h2'}, 30107 {title: 'Heading 3', format: 'h3'}, 30108 {title: 'Heading 4', format: 'h4'}, 30109 {title: 'Heading 5', format: 'h5'}, 30110 {title: 'Heading 6', format: 'h6'} 30111 ]}, 30112 30113 {title: 'Inline', items: [ 30114 {title: 'Bold', icon: 'bold', format: 'bold'}, 30115 {title: 'Italic', icon: 'italic', format: 'italic'}, 30116 {title: 'Underline', icon: 'underline', format: 'underline'}, 30117 {title: 'Strikethrough', icon: 'strikethrough', format: 'strikethrough'}, 30118 {title: 'Superscript', icon: 'superscript', format: 'superscript'}, 30119 {title: 'Subscript', icon: 'subscript', format: 'subscript'}, 30120 {title: 'Code', icon: 'code', format: 'code'} 30121 ]}, 30122 30123 {title: 'Blocks', items: [ 30124 {title: 'Paragraph', format: 'p'}, 30125 {title: 'Blockquote', format: 'blockquote'}, 30126 {title: 'Div', format: 'div'}, 30127 {title: 'Pre', format: 'pre'} 30128 ]}, 30129 30130 {title: 'Alignment', items: [ 30131 {title: 'Left', icon: 'alignleft', format: 'alignleft'}, 30132 {title: 'Center', icon: 'aligncenter', format: 'aligncenter'}, 30133 {title: 'Right', icon: 'alignright', format: 'alignright'}, 30134 {title: 'Justify', icon: 'alignjustify', format: 'alignjustify'} 30135 ]} 30136 ]; 30137 30138 function createMenu(formats) { 30139 var menu = []; 30140 30141 if (!formats) { 30142 return; 30143 } 30144 30145 each(formats, function(format) { 30146 var menuItem = { 30147 text: format.title, 30148 icon: format.icon 30149 }; 30150 30151 if (format.items) { 30152 menuItem.menu = createMenu(format.items); 30153 } else { 30154 var formatName = format.format || "custom" + count++; 30155 30156 if (!format.format) { 30157 format.name = formatName; 30158 newFormats.push(format); 30159 } 30160 30161 menuItem.format = formatName; 30162 } 30163 30164 menu.push(menuItem); 30165 }); 30166 30167 return menu; 30168 } 30169 30170 function createStylesMenu() { 30171 var menu; 30172 30173 if (editor.settings.style_formats_merge) { 30174 if (editor.settings.style_formats) { 30175 menu = createMenu(defaultStyleFormats.concat(editor.settings.style_formats)); 30176 } else { 30177 menu = createMenu(defaultStyleFormats); 30178 } 30179 } else { 30180 menu = createMenu(editor.settings.style_formats || defaultStyleFormats); 30181 } 30182 30183 return menu; 30184 } 30185 30186 editor.on('init', function() { 30187 each(newFormats, function(format) { 30188 editor.formatter.register(format.name, format); 30189 }); 30190 }); 30191 30192 return { 30193 type: 'menu', 30194 items: createStylesMenu(), 30195 onPostRender: function(e) { 30196 editor.fire('renderFormatsMenu', {control: e.control}); 30197 }, 30198 itemDefaults: { 30199 preview: true, 30200 30201 textStyle: function() { 30202 if (this.settings.format) { 30203 return editor.formatter.getCssText(this.settings.format); 30204 } 30205 }, 30206 30207 onPostRender: function() { 30208 var self = this, formatName = this.settings.format; 30209 30210 if (formatName) { 30211 self.parent().on('show', function() { 30212 self.disabled(!editor.formatter.canApply(formatName)); 30213 self.active(editor.formatter.match(formatName)); 30214 }); 30215 } 30216 }, 30217 30218 onclick: function() { 30219 if (this.settings.format) { 30220 toggleFormat(this.settings.format); 30221 } 30222 } 30223 } 30224 }; 30225 } 30226 30227 formatMenu = createFormatMenu(); 30228 30229 // Simple format controls <control/format>:<UI text> 30230 each({ 30231 bold: 'Bold', 30232 italic: 'Italic', 30233 underline: 'Underline', 30234 strikethrough: 'Strikethrough', 30235 subscript: 'Subscript', 30236 superscript: 'Superscript' 30237 }, function(text, name) { 30238 editor.addButton(name, { 30239 tooltip: text, 30240 onPostRender: function() { 30241 var self = this; 30242 30243 // TODO: Fix this 30244 if (editor.formatter) { 30245 editor.formatter.formatChanged(name, function(state) { 30246 self.active(state); 30247 }); 30248 } else { 30249 editor.on('init', function() { 30250 editor.formatter.formatChanged(name, function(state) { 30251 self.active(state); 30252 }); 30253 }); 30254 } 30255 }, 30256 onclick: function() { 30257 toggleFormat(name); 30258 } 30259 }); 30260 }); 30261 30262 // Simple command controls <control>:[<UI text>,<Command>] 30263 each({ 30264 outdent: ['Decrease indent', 'Outdent'], 30265 indent: ['Increase indent', 'Indent'], 30266 cut: ['Cut', 'Cut'], 30267 copy: ['Copy', 'Copy'], 30268 paste: ['Paste', 'Paste'], 30269 help: ['Help', 'mceHelp'], 30270 selectall: ['Select all', 'SelectAll'], 30271 hr: ['Insert horizontal rule', 'InsertHorizontalRule'], 30272 removeformat: ['Clear formatting', 'RemoveFormat'], 30273 visualaid: ['Visual aids', 'mceToggleVisualAid'], 30274 newdocument: ['New document', 'mceNewDocument'] 30275 }, function(item, name) { 30276 editor.addButton(name, { 30277 tooltip: item[0], 30278 cmd: item[1] 30279 }); 30280 }); 30281 30282 // Simple command controls with format state 30283 each({ 30284 blockquote: ['Blockquote', 'mceBlockQuote'], 30285 numlist: ['Numbered list', 'InsertOrderedList'], 30286 bullist: ['Bullet list', 'InsertUnorderedList'], 30287 subscript: ['Subscript', 'Subscript'], 30288 superscript: ['Superscript', 'Superscript'], 30289 alignleft: ['Align left', 'JustifyLeft'], 30290 aligncenter: ['Align center', 'JustifyCenter'], 30291 alignright: ['Align right', 'JustifyRight'], 30292 alignjustify: ['Justify', 'JustifyFull'] 30293 }, function(item, name) { 30294 editor.addButton(name, { 30295 tooltip: item[0], 30296 cmd: item[1], 30297 onPostRender: function() { 30298 var self = this; 30299 30300 // TODO: Fix this 30301 if (editor.formatter) { 30302 editor.formatter.formatChanged(name, function(state) { 30303 self.active(state); 30304 }); 30305 } else { 30306 editor.on('init', function() { 30307 editor.formatter.formatChanged(name, function(state) { 30308 self.active(state); 30309 }); 30310 }); 30311 } 30312 } 30313 }); 30314 }); 30315 30316 function hasUndo() { 30317 return editor.undoManager ? editor.undoManager.hasUndo() : false; 30318 } 30319 30320 function hasRedo() { 30321 return editor.undoManager ? editor.undoManager.hasRedo() : false; 30322 } 30323 30324 function toggleUndoState() { 30325 var self = this; 30326 30327 self.disabled(!hasUndo()); 30328 editor.on('Undo Redo AddUndo TypingUndo', function() { 30329 self.disabled(!hasUndo()); 30330 }); 30331 } 30332 30333 function toggleRedoState() { 30334 var self = this; 30335 30336 self.disabled(!hasRedo()); 30337 editor.on('Undo Redo AddUndo TypingUndo', function() { 30338 self.disabled(!hasRedo()); 30339 }); 30340 } 30341 30342 function toggleVisualAidState() { 30343 var self = this; 30344 30345 editor.on('VisualAid', function(e) { 30346 self.active(e.hasVisual); 30347 }); 30348 30349 self.active(editor.hasVisual); 30350 } 30351 30352 editor.addButton('undo', { 30353 tooltip: 'Undo', 30354 onPostRender: toggleUndoState, 30355 cmd: 'undo' 30356 }); 30357 30358 editor.addButton('redo', { 30359 tooltip: 'Redo', 30360 onPostRender: toggleRedoState, 30361 cmd: 'redo' 30362 }); 30363 30364 editor.addMenuItem('newdocument', { 30365 text: 'New document', 30366 shortcut: 'Ctrl+N', 30367 icon: 'newdocument', 30368 cmd: 'mceNewDocument' 30369 }); 30370 30371 editor.addMenuItem('undo', { 30372 text: 'Undo', 30373 icon: 'undo', 30374 shortcut: 'Ctrl+Z', 30375 onPostRender: toggleUndoState, 30376 cmd: 'undo' 30377 }); 30378 30379 editor.addMenuItem('redo', { 30380 text: 'Redo', 30381 icon: 'redo', 30382 shortcut: 'Ctrl+Y', 30383 onPostRender: toggleRedoState, 30384 cmd: 'redo' 30385 }); 30386 30387 editor.addMenuItem('visualaid', { 30388 text: 'Visual aids', 30389 selectable: true, 30390 onPostRender: toggleVisualAidState, 30391 cmd: 'mceToggleVisualAid' 30392 }); 30393 30394 each({ 30395 cut: ['Cut', 'Cut', 'Ctrl+X'], 30396 copy: ['Copy', 'Copy', 'Ctrl+C'], 30397 paste: ['Paste', 'Paste', 'Ctrl+V'], 30398 selectall: ['Select all', 'SelectAll', 'Ctrl+A'], 30399 bold: ['Bold', 'Bold', 'Ctrl+B'], 30400 italic: ['Italic', 'Italic', 'Ctrl+I'], 30401 underline: ['Underline', 'Underline'], 30402 strikethrough: ['Strikethrough', 'Strikethrough'], 30403 subscript: ['Subscript', 'Subscript'], 30404 superscript: ['Superscript', 'Superscript'], 30405 removeformat: ['Clear formatting', 'RemoveFormat'] 30406 }, function(item, name) { 30407 editor.addMenuItem(name, { 30408 text: item[0], 30409 icon: name, 30410 shortcut: item[2], 30411 cmd: item[1] 30412 }); 30413 }); 30414 30415 editor.on('mousedown', function() { 30416 FloatPanel.hideAll(); 30417 }); 30418 30419 function toggleFormat(fmt) { 30420 if (fmt.control) { 30421 fmt = fmt.control.value(); 30422 } 30423 30424 if (fmt) { 30425 editor.execCommand('mceToggleFormat', false, fmt); 30426 } 30427 } 30428 30429 editor.addButton('styleselect', { 30430 type: 'menubutton', 30431 text: 'Formats', 30432 menu: formatMenu 30433 }); 30434 30435 editor.addButton('formatselect', function() { 30436 var items = [], blocks = createFormats(editor.settings.block_formats || 30437 'Paragraph=p;' + 30438 'Address=address;' + 30439 'Pre=pre;' + 30440 'Heading 1=h1;' + 30441 'Heading 2=h2;' + 30442 'Heading 3=h3;' + 30443 'Heading 4=h4;' + 30444 'Heading 5=h5;' + 30445 'Heading 6=h6' 30446 ); 30447 30448 each(blocks, function(block) { 30449 items.push({ 30450 text: block[0], 30451 value: block[1], 30452 textStyle: function() { 30453 return editor.formatter.getCssText(block[1]); 30454 } 30455 }); 30456 }); 30457 30458 return { 30459 type: 'listbox', 30460 text: blocks[0][0], 30461 values: items, 30462 fixedWidth: true, 30463 onselect: toggleFormat, 30464 onPostRender: createListBoxChangeHandler(items) 30465 }; 30466 }); 30467 30468 editor.addButton('fontselect', function() { 30469 var defaultFontsFormats = 30470 'Andale Mono=andale mono,times;' + 30471 'Arial=arial,helvetica,sans-serif;' + 30472 'Arial Black=arial black,avant garde;' + 30473 'Book Antiqua=book antiqua,palatino;' + 30474 'Comic Sans MS=comic sans ms,sans-serif;' + 30475 'Courier New=courier new,courier;' + 30476 'Georgia=georgia,palatino;' + 30477 'Helvetica=helvetica;' + 30478 'Impact=impact,chicago;' + 30479 'Symbol=symbol;' + 30480 'Tahoma=tahoma,arial,helvetica,sans-serif;' + 30481 'Terminal=terminal,monaco;' + 30482 'Times New Roman=times new roman,times;' + 30483 'Trebuchet MS=trebuchet ms,geneva;' + 30484 'Verdana=verdana,geneva;' + 30485 'Webdings=webdings;' + 30486 'Wingdings=wingdings,zapf dingbats'; 30487 30488 var items = [], fonts = createFormats(editor.settings.font_formats || defaultFontsFormats); 30489 30490 each(fonts, function(font) { 30491 items.push({ 30492 text: {raw: font[0]}, 30493 value: font[1], 30494 textStyle: font[1].indexOf('dings') == -1 ? 'font-family:' + font[1] : '' 30495 }); 30496 }); 30497 30498 return { 30499 type: 'listbox', 30500 text: 'Font Family', 30501 tooltip: 'Font Family', 30502 values: items, 30503 fixedWidth: true, 30504 onPostRender: createListBoxChangeHandler(items, 'fontname'), 30505 onselect: function(e) { 30506 if (e.control.settings.value) { 30507 editor.execCommand('FontName', false, e.control.settings.value); 30508 } 30509 } 30510 }; 30511 }); 30512 30513 editor.addButton('fontsizeselect', function() { 30514 var items = [], defaultFontsizeFormats = '8pt 10pt 12pt 14pt 18pt 24pt 36pt'; 30515 var fontsize_formats = editor.settings.fontsize_formats || defaultFontsizeFormats; 30516 30517 each(fontsize_formats.split(' '), function(item) { 30518 items.push({text: item, value: item}); 30519 }); 30520 30521 return { 30522 type: 'listbox', 30523 text: 'Font Sizes', 30524 tooltip: 'Font Sizes', 30525 values: items, 30526 fixedWidth: true, 30527 onPostRender: createListBoxChangeHandler(items, 'fontsize'), 30528 onclick: function(e) { 30529 if (e.control.settings.value) { 30530 editor.execCommand('FontSize', false, e.control.settings.value); 30531 } 30532 } 30533 }; 30534 }); 30535 30536 editor.addMenuItem('formats', { 30537 text: 'Formats', 30538 menu: formatMenu 30539 }); 30540 } 30541 }); 30542 30543 // Included from: js/tinymce/classes/ui/GridLayout.js 30544 30545 /** 30546 * GridLayout.js 30547 * 30548 * Copyright, Moxiecode Systems AB 30549 * Released under LGPL License. 30550 * 30551 * License: http://www.tinymce.com/license 30552 * Contributing: http://www.tinymce.com/contributing 30553 */ 30554 30555 /** 30556 * This layout manager places controls in a grid. 30557 * 30558 * @setting {Number} spacing Spacing between controls. 30559 * @setting {Number} spacingH Horizontal spacing between controls. 30560 * @setting {Number} spacingV Vertical spacing between controls. 30561 * @setting {Number} columns Number of columns to use. 30562 * @setting {String/Array} alignH start|end|center|stretch or array of values for each column. 30563 * @setting {String/Array} alignV start|end|center|stretch or array of values for each column. 30564 * @setting {String} pack start|end 30565 * 30566 * @class tinymce.ui.GridLayout 30567 * @extends tinymce.ui.AbsoluteLayout 30568 */ 30569 define("tinymce/ui/GridLayout", [ 30570 "tinymce/ui/AbsoluteLayout" 30571 ], function(AbsoluteLayout) { 30572 "use strict"; 30573 30574 return AbsoluteLayout.extend({ 30575 /** 30576 * Recalculates the positions of the controls in the specified container. 30577 * 30578 * @method recalc 30579 * @param {tinymce.ui.Container} container Container instance to recalc. 30580 */ 30581 recalc: function(container) { 30582 var settings = container.settings, rows, cols, items, contLayoutRect, width, height, rect, 30583 ctrlLayoutRect, ctrl, x, y, posX, posY, ctrlSettings, contPaddingBox, align, spacingH, spacingV, alignH, alignV, maxX, maxY, 30584 colWidths = [], rowHeights = [], ctrlMinWidth, ctrlMinHeight, availableWidth, availableHeight; 30585 30586 // Get layout settings 30587 settings = container.settings; 30588 items = container.items().filter(':visible'); 30589 contLayoutRect = container.layoutRect(); 30590 cols = settings.columns || Math.ceil(Math.sqrt(items.length)); 30591 rows = Math.ceil(items.length / cols); 30592 spacingH = settings.spacingH || settings.spacing || 0; 30593 spacingV = settings.spacingV || settings.spacing || 0; 30594 alignH = settings.alignH || settings.align; 30595 alignV = settings.alignV || settings.align; 30596 contPaddingBox = container._paddingBox; 30597 30598 if (alignH && typeof(alignH) == "string") { 30599 alignH = [alignH]; 30600 } 30601 30602 if (alignV && typeof(alignV) == "string") { 30603 alignV = [alignV]; 30604 } 30605 30606 // Zero padd columnWidths 30607 for (x = 0; x < cols; x++) { 30608 colWidths.push(0); 30609 } 30610 30611 // Zero padd rowHeights 30612 for (y = 0; y < rows; y++) { 30613 rowHeights.push(0); 30614 } 30615 30616 // Calculate columnWidths and rowHeights 30617 for (y = 0; y < rows; y++) { 30618 for (x = 0; x < cols; x++) { 30619 ctrl = items[y * cols + x]; 30620 30621 // Out of bounds 30622 if (!ctrl) { 30623 break; 30624 } 30625 30626 ctrlLayoutRect = ctrl.layoutRect(); 30627 ctrlMinWidth = ctrlLayoutRect.minW; 30628 ctrlMinHeight = ctrlLayoutRect.minH; 30629 30630 colWidths[x] = ctrlMinWidth > colWidths[x] ? ctrlMinWidth : colWidths[x]; 30631 rowHeights[y] = ctrlMinHeight > rowHeights[y] ? ctrlMinHeight : rowHeights[y]; 30632 } 30633 } 30634 30635 // Calculate maxX 30636 availableWidth = contLayoutRect.innerW - contPaddingBox.left - contPaddingBox.right; 30637 for (maxX = 0, x = 0; x < cols; x++) { 30638 maxX += colWidths[x] + (x > 0 ? spacingH : 0); 30639 availableWidth -= (x > 0 ? spacingH : 0) + colWidths[x]; 30640 } 30641 30642 // Calculate maxY 30643 availableHeight = contLayoutRect.innerH - contPaddingBox.top - contPaddingBox.bottom; 30644 for (maxY = 0, y = 0; y < rows; y++) { 30645 maxY += rowHeights[y] + (y > 0 ? spacingV : 0); 30646 availableHeight -= (y > 0 ? spacingV : 0) + rowHeights[y]; 30647 } 30648 30649 maxX += contPaddingBox.left + contPaddingBox.right; 30650 maxY += contPaddingBox.top + contPaddingBox.bottom; 30651 30652 // Calculate minW/minH 30653 rect = {}; 30654 rect.minW = maxX + (contLayoutRect.w - contLayoutRect.innerW); 30655 rect.minH = maxY + (contLayoutRect.h - contLayoutRect.innerH); 30656 30657 rect.contentW = rect.minW - contLayoutRect.deltaW; 30658 rect.contentH = rect.minH - contLayoutRect.deltaH; 30659 rect.minW = Math.min(rect.minW, contLayoutRect.maxW); 30660 rect.minH = Math.min(rect.minH, contLayoutRect.maxH); 30661 rect.minW = Math.max(rect.minW, contLayoutRect.startMinWidth); 30662 rect.minH = Math.max(rect.minH, contLayoutRect.startMinHeight); 30663 30664 // Resize container container if minSize was changed 30665 if (contLayoutRect.autoResize && (rect.minW != contLayoutRect.minW || rect.minH != contLayoutRect.minH)) { 30666 rect.w = rect.minW; 30667 rect.h = rect.minH; 30668 30669 container.layoutRect(rect); 30670 this.recalc(container); 30671 30672 // Forced recalc for example if items are hidden/shown 30673 if (container._lastRect === null) { 30674 var parentCtrl = container.parent(); 30675 if (parentCtrl) { 30676 parentCtrl._lastRect = null; 30677 parentCtrl.recalc(); 30678 } 30679 } 30680 30681 return; 30682 } 30683 30684 // Update contentW/contentH so absEnd moves correctly 30685 if (contLayoutRect.autoResize) { 30686 rect = container.layoutRect(rect); 30687 rect.contentW = rect.minW - contLayoutRect.deltaW; 30688 rect.contentH = rect.minH - contLayoutRect.deltaH; 30689 } 30690 30691 var flexV; 30692 30693 if (settings.packV == 'start') { 30694 flexV = 0; 30695 } else { 30696 flexV = availableHeight > 0 ? Math.floor(availableHeight / rows) : 0; 30697 } 30698 30699 // Calculate totalFlex 30700 var totalFlex = 0; 30701 var flexWidths = settings.flexWidths; 30702 if (flexWidths) { 30703 for (x = 0; x < flexWidths.length; x++) { 30704 totalFlex += flexWidths[x]; 30705 } 30706 } else { 30707 totalFlex = cols; 30708 } 30709 30710 // Calculate new column widths based on flex values 30711 var ratio = availableWidth / totalFlex; 30712 for (x = 0; x < cols; x++) { 30713 colWidths[x] += flexWidths ? flexWidths[x] * ratio : ratio; 30714 } 30715 30716 // Move/resize controls 30717 posY = contPaddingBox.top; 30718 for (y = 0; y < rows; y++) { 30719 posX = contPaddingBox.left; 30720 height = rowHeights[y] + flexV; 30721 30722 for (x = 0; x < cols; x++) { 30723 ctrl = items[y * cols + x]; 30724 30725 // No more controls to render then break 30726 if (!ctrl) { 30727 break; 30728 } 30729 30730 // Get control settings and calculate x, y 30731 ctrlSettings = ctrl.settings; 30732 ctrlLayoutRect = ctrl.layoutRect(); 30733 width = Math.max(colWidths[x], ctrlLayoutRect.startMinWidth); 30734 ctrlLayoutRect.x = posX; 30735 ctrlLayoutRect.y = posY; 30736 30737 // Align control horizontal 30738 align = ctrlSettings.alignH || (alignH ? (alignH[x] || alignH[0]) : null); 30739 if (align == "center") { 30740 ctrlLayoutRect.x = posX + (width / 2) - (ctrlLayoutRect.w / 2); 30741 } else if (align == "right") { 30742 ctrlLayoutRect.x = posX + width - ctrlLayoutRect.w; 30743 } else if (align == "stretch") { 30744 ctrlLayoutRect.w = width; 30745 } 30746 30747 // Align control vertical 30748 align = ctrlSettings.alignV || (alignV ? (alignV[x] || alignV[0]) : null); 30749 if (align == "center") { 30750 ctrlLayoutRect.y = posY + (height / 2) - (ctrlLayoutRect.h / 2); 30751 } else if (align == "bottom") { 30752 ctrlLayoutRect.y = posY + height - ctrlLayoutRect.h; 30753 } else if (align == "stretch") { 30754 ctrlLayoutRect.h = height; 30755 } 30756 30757 ctrl.layoutRect(ctrlLayoutRect); 30758 30759 posX += width + spacingH; 30760 30761 if (ctrl.recalc) { 30762 ctrl.recalc(); 30763 } 30764 } 30765 30766 posY += height + spacingV; 30767 } 30768 } 30769 }); 30770 }); 30771 30772 // Included from: js/tinymce/classes/ui/Iframe.js 30773 30774 /** 30775 * Iframe.js 30776 * 30777 * Copyright, Moxiecode Systems AB 30778 * Released under LGPL License. 30779 * 30780 * License: http://www.tinymce.com/license 30781 * Contributing: http://www.tinymce.com/contributing 30782 */ 30783 30784 /*jshint scripturl:true */ 30785 30786 /** 30787 * This class creates an iframe. 30788 * 30789 * @setting {String} url Url to open in the iframe. 30790 * 30791 * @-x-less Iframe.less 30792 * @class tinymce.ui.Iframe 30793 * @extends tinymce.ui.Widget 30794 */ 30795 define("tinymce/ui/Iframe", [ 30796 "tinymce/ui/Widget" 30797 ], function(Widget) { 30798 "use strict"; 30799 30800 return Widget.extend({ 30801 /** 30802 * Renders the control as a HTML string. 30803 * 30804 * @method renderHtml 30805 * @return {String} HTML representing the control. 30806 */ 30807 renderHtml: function() { 30808 var self = this; 30809 30810 self.addClass('iframe'); 30811 self.canFocus = false; 30812 30813 /*eslint no-script-url:0 */ 30814 return ( 30815 '<iframe id="' + self._id + '" class="' + self.classes() + '" tabindex="-1" src="' + 30816 (self.settings.url || "javascript:\'\'") + '" frameborder="0"></iframe>' 30817 ); 30818 }, 30819 30820 /** 30821 * Setter for the iframe source. 30822 * 30823 * @method src 30824 * @param {String} src Source URL for iframe. 30825 */ 30826 src: function(src) { 30827 this.getEl().src = src; 30828 }, 30829 30830 /** 30831 * Inner HTML for the iframe. 30832 * 30833 * @method html 30834 * @param {String} html HTML string to set as HTML inside the iframe. 30835 * @param {function} callback Optional callback to execute when the iframe body is filled with contents. 30836 * @return {tinymce.ui.Iframe} Current iframe control. 30837 */ 30838 html: function(html, callback) { 30839 var self = this, body = this.getEl().contentWindow.document.body; 30840 30841 // Wait for iframe to initialize IE 10 takes time 30842 if (!body) { 30843 setTimeout(function() { 30844 self.html(html); 30845 }, 0); 30846 } else { 30847 body.innerHTML = html; 30848 30849 if (callback) { 30850 callback(); 30851 } 30852 } 30853 30854 return this; 30855 } 30856 }); 30857 }); 30858 30859 // Included from: js/tinymce/classes/ui/Label.js 30860 30861 /** 30862 * Label.js 30863 * 30864 * Copyright, Moxiecode Systems AB 30865 * Released under LGPL License. 30866 * 30867 * License: http://www.tinymce.com/license 30868 * Contributing: http://www.tinymce.com/contributing 30869 */ 30870 30871 /** 30872 * This class creates a label element. A label is a simple text control 30873 * that can be bound to other controls. 30874 * 30875 * @-x-less Label.less 30876 * @class tinymce.ui.Label 30877 * @extends tinymce.ui.Widget 30878 */ 30879 define("tinymce/ui/Label", [ 30880 "tinymce/ui/Widget", 30881 "tinymce/ui/DomUtils" 30882 ], function(Widget, DomUtils) { 30883 "use strict"; 30884 30885 return Widget.extend({ 30886 /** 30887 * Constructs a instance with the specified settings. 30888 * 30889 * @constructor 30890 * @param {Object} settings Name/value object with settings. 30891 * @param {Boolean} multiline Multiline label. 30892 */ 30893 init: function(settings) { 30894 var self = this; 30895 30896 self._super(settings); 30897 self.addClass('widget'); 30898 self.addClass('label'); 30899 self.canFocus = false; 30900 30901 if (settings.multiline) { 30902 self.addClass('autoscroll'); 30903 } 30904 30905 if (settings.strong) { 30906 self.addClass('strong'); 30907 } 30908 }, 30909 30910 /** 30911 * Initializes the current controls layout rect. 30912 * This will be executed by the layout managers to determine the 30913 * default minWidth/minHeight etc. 30914 * 30915 * @method initLayoutRect 30916 * @return {Object} Layout rect instance. 30917 */ 30918 initLayoutRect: function() { 30919 var self = this, layoutRect = self._super(); 30920 30921 if (self.settings.multiline) { 30922 var size = DomUtils.getSize(self.getEl()); 30923 30924 // Check if the text fits within maxW if not then try word wrapping it 30925 if (size.width > layoutRect.maxW) { 30926 layoutRect.minW = layoutRect.maxW; 30927 self.addClass('multiline'); 30928 } 30929 30930 self.getEl().style.width = layoutRect.minW + 'px'; 30931 layoutRect.startMinH = layoutRect.h = layoutRect.minH = Math.min(layoutRect.maxH, DomUtils.getSize(self.getEl()).height); 30932 } 30933 30934 return layoutRect; 30935 }, 30936 30937 /** 30938 * Repaints the control after a layout operation. 30939 * 30940 * @method repaint 30941 */ 30942 repaint: function() { 30943 var self = this; 30944 30945 if (!self.settings.multiline) { 30946 self.getEl().style.lineHeight = self.layoutRect().h + 'px'; 30947 } 30948 30949 return self._super(); 30950 }, 30951 30952 /** 30953 * Sets/gets the current label text. 30954 * 30955 * @method text 30956 * @param {String} [text] New label text. 30957 * @return {String|tinymce.ui.Label} Current text or current label instance. 30958 */ 30959 text: function(text) { 30960 var self = this; 30961 30962 if (self._rendered && text) { 30963 this.innerHtml(self.encode(text)); 30964 } 30965 30966 return self._super(text); 30967 }, 30968 30969 /** 30970 * Renders the control as a HTML string. 30971 * 30972 * @method renderHtml 30973 * @return {String} HTML representing the control. 30974 */ 30975 renderHtml: function() { 30976 var self = this, forId = self.settings.forId; 30977 30978 return ( 30979 '<label id="' + self._id + '" class="' + self.classes() + '"' + (forId ? ' for="' + forId + '"' : '') + '>' + 30980 self.encode(self._text) + 30981 '</label>' 30982 ); 30983 } 30984 }); 30985 }); 30986 30987 // Included from: js/tinymce/classes/ui/Toolbar.js 30988 30989 /** 30990 * Toolbar.js 30991 * 30992 * Copyright, Moxiecode Systems AB 30993 * Released under LGPL License. 30994 * 30995 * License: http://www.tinymce.com/license 30996 * Contributing: http://www.tinymce.com/contributing 30997 */ 30998 30999 /** 31000 * Creates a new toolbar. 31001 * 31002 * @class tinymce.ui.Toolbar 31003 * @extends tinymce.ui.Container 31004 */ 31005 define("tinymce/ui/Toolbar", [ 31006 "tinymce/ui/Container" 31007 ], function(Container) { 31008 "use strict"; 31009 31010 return Container.extend({ 31011 Defaults: { 31012 role: 'toolbar', 31013 layout: 'flow' 31014 }, 31015 31016 /** 31017 * Constructs a instance with the specified settings. 31018 * 31019 * @constructor 31020 * @param {Object} settings Name/value object with settings. 31021 */ 31022 init: function(settings) { 31023 var self = this; 31024 31025 self._super(settings); 31026 self.addClass('toolbar'); 31027 }, 31028 31029 /** 31030 * Called after the control has been rendered. 31031 * 31032 * @method postRender 31033 */ 31034 postRender: function() { 31035 var self = this; 31036 31037 self.items().addClass('toolbar-item'); 31038 31039 return self._super(); 31040 } 31041 }); 31042 }); 31043 31044 // Included from: js/tinymce/classes/ui/MenuBar.js 31045 31046 /** 31047 * MenuBar.js 31048 * 31049 * Copyright, Moxiecode Systems AB 31050 * Released under LGPL License. 31051 * 31052 * License: http://www.tinymce.com/license 31053 * Contributing: http://www.tinymce.com/contributing 31054 */ 31055 31056 /** 31057 * Creates a new menubar. 31058 * 31059 * @-x-less MenuBar.less 31060 * @class tinymce.ui.MenuBar 31061 * @extends tinymce.ui.Container 31062 */ 31063 define("tinymce/ui/MenuBar", [ 31064 "tinymce/ui/Toolbar" 31065 ], function(Toolbar) { 31066 "use strict"; 31067 31068 return Toolbar.extend({ 31069 Defaults: { 31070 role: 'menubar', 31071 containerCls: 'menubar', 31072 ariaRoot: true, 31073 defaults: { 31074 type: 'menubutton' 31075 } 31076 } 31077 }); 31078 }); 31079 31080 // Included from: js/tinymce/classes/ui/MenuButton.js 31081 31082 /** 31083 * MenuButton.js 31084 * 31085 * Copyright, Moxiecode Systems AB 31086 * Released under LGPL License. 31087 * 31088 * License: http://www.tinymce.com/license 31089 * Contributing: http://www.tinymce.com/contributing 31090 */ 31091 31092 /** 31093 * Creates a new menu button. 31094 * 31095 * @-x-less MenuButton.less 31096 * @class tinymce.ui.MenuButton 31097 * @extends tinymce.ui.Button 31098 */ 31099 define("tinymce/ui/MenuButton", [ 31100 "tinymce/ui/Button", 31101 "tinymce/ui/Factory", 31102 "tinymce/ui/MenuBar" 31103 ], function(Button, Factory, MenuBar) { 31104 "use strict"; 31105 31106 // TODO: Maybe add as some global function 31107 function isChildOf(node, parent) { 31108 while (node) { 31109 if (parent === node) { 31110 return true; 31111 } 31112 31113 node = node.parentNode; 31114 } 31115 31116 return false; 31117 } 31118 31119 var MenuButton = Button.extend({ 31120 /** 31121 * Constructs a instance with the specified settings. 31122 * 31123 * @constructor 31124 * @param {Object} settings Name/value object with settings. 31125 */ 31126 init: function(settings) { 31127 var self = this; 31128 31129 self._renderOpen = true; 31130 self._super(settings); 31131 31132 self.addClass('menubtn'); 31133 31134 if (settings.fixedWidth) { 31135 self.addClass('fixed-width'); 31136 } 31137 31138 self.aria('haspopup', true); 31139 self.hasPopup = true; 31140 }, 31141 31142 /** 31143 * Shows the menu for the button. 31144 * 31145 * @method showMenu 31146 */ 31147 showMenu: function() { 31148 var self = this, settings = self.settings, menu; 31149 31150 if (self.menu && self.menu.visible()) { 31151 return self.hideMenu(); 31152 } 31153 31154 if (!self.menu) { 31155 menu = settings.menu || []; 31156 31157 // Is menu array then auto constuct menu control 31158 if (menu.length) { 31159 menu = { 31160 type: 'menu', 31161 items: menu 31162 }; 31163 } else { 31164 menu.type = menu.type || 'menu'; 31165 } 31166 31167 self.menu = Factory.create(menu).parent(self).renderTo(); 31168 self.fire('createmenu'); 31169 self.menu.reflow(); 31170 self.menu.on('cancel', function(e) { 31171 if (e.control.parent() === self.menu) { 31172 e.stopPropagation(); 31173 self.focus(); 31174 self.hideMenu(); 31175 } 31176 }); 31177 31178 // Move focus to button when a menu item is selected/clicked 31179 self.menu.on('select', function() { 31180 self.focus(); 31181 }); 31182 31183 self.menu.on('show hide', function(e) { 31184 if (e.control == self.menu) { 31185 self.activeMenu(e.type == 'show'); 31186 } 31187 31188 self.aria('expanded', e.type == 'show'); 31189 }).fire('show'); 31190 } 31191 31192 self.menu.show(); 31193 self.menu.layoutRect({w: self.layoutRect().w}); 31194 self.menu.moveRel(self.getEl(), self.isRtl() ? ['br-tr', 'tr-br'] : ['bl-tl', 'tl-bl']); 31195 }, 31196 31197 /** 31198 * Hides the menu for the button. 31199 * 31200 * @method hideMenu 31201 */ 31202 hideMenu: function() { 31203 var self = this; 31204 31205 if (self.menu) { 31206 self.menu.items().each(function(item) { 31207 if (item.hideMenu) { 31208 item.hideMenu(); 31209 } 31210 }); 31211 31212 self.menu.hide(); 31213 } 31214 }, 31215 31216 /** 31217 * Sets the active menu state. 31218 * 31219 * @private 31220 */ 31221 activeMenu: function(state) { 31222 this.toggleClass('active', state); 31223 }, 31224 31225 /** 31226 * Renders the control as a HTML string. 31227 * 31228 * @method renderHtml 31229 * @return {String} HTML representing the control. 31230 */ 31231 renderHtml: function() { 31232 var self = this, id = self._id, prefix = self.classPrefix; 31233 var icon = self.settings.icon ? prefix + 'ico ' + prefix + 'i-' + self.settings.icon : ''; 31234 31235 self.aria('role', self.parent() instanceof MenuBar ? 'menuitem' : 'button'); 31236 31237 return ( 31238 '<div id="' + id + '" class="' + self.classes() + '" tabindex="-1" aria-labelledby="' + id + '">' + 31239 '<button id="' + id + '-open" role="presentation" type="button" tabindex="-1">' + 31240 (icon ? '<i class="' + icon + '"></i>' : '') + 31241 '<span>' + (self._text ? (icon ? '\u00a0' : '') + self.encode(self._text) : '') + '</span>' + 31242 ' <i class="' + prefix + 'caret"></i>' + 31243 '</button>' + 31244 '</div>' 31245 ); 31246 }, 31247 31248 /** 31249 * Gets invoked after the control has been rendered. 31250 * 31251 * @method postRender 31252 */ 31253 postRender: function() { 31254 var self = this; 31255 31256 self.on('click', function(e) { 31257 if (e.control === self && isChildOf(e.target, self.getEl())) { 31258 self.showMenu(); 31259 31260 if (e.aria) { 31261 self.menu.items()[0].focus(); 31262 } 31263 } 31264 }); 31265 31266 self.on('mouseenter', function(e) { 31267 var overCtrl = e.control, parent = self.parent(), hasVisibleSiblingMenu; 31268 31269 if (overCtrl && parent && overCtrl instanceof MenuButton && overCtrl.parent() == parent) { 31270 parent.items().filter('MenuButton').each(function(ctrl) { 31271 if (ctrl.hideMenu && ctrl != overCtrl) { 31272 if (ctrl.menu && ctrl.menu.visible()) { 31273 hasVisibleSiblingMenu = true; 31274 } 31275 31276 ctrl.hideMenu(); 31277 } 31278 }); 31279 31280 if (hasVisibleSiblingMenu) { 31281 overCtrl.focus(); // Fix for: #5887 31282 overCtrl.showMenu(); 31283 } 31284 } 31285 }); 31286 31287 return self._super(); 31288 }, 31289 31290 /** 31291 * Sets/gets the current button text. 31292 * 31293 * @method text 31294 * @param {String} [text] New button text. 31295 * @return {String|tinymce.ui.MenuButton} Current text or current MenuButton instance. 31296 */ 31297 text: function(text) { 31298 var self = this, i, children; 31299 31300 if (self._rendered) { 31301 children = self.getEl('open').getElementsByTagName('span'); 31302 for (i = 0; i < children.length; i++) { 31303 children[i].innerHTML = (self.settings.icon && text ? '\u00a0' : '') + self.encode(text); 31304 } 31305 } 31306 31307 return this._super(text); 31308 }, 31309 31310 /** 31311 * Removes the control and it's menus. 31312 * 31313 * @method remove 31314 */ 31315 remove: function() { 31316 this._super(); 31317 31318 if (this.menu) { 31319 this.menu.remove(); 31320 } 31321 } 31322 }); 31323 31324 return MenuButton; 31325 }); 31326 31327 // Included from: js/tinymce/classes/ui/ListBox.js 31328 31329 /** 31330 * ListBox.js 31331 * 31332 * Copyright, Moxiecode Systems AB 31333 * Released under LGPL License. 31334 * 31335 * License: http://www.tinymce.com/license 31336 * Contributing: http://www.tinymce.com/contributing 31337 */ 31338 31339 /** 31340 * Creates a new list box control. 31341 * 31342 * @-x-less ListBox.less 31343 * @class tinymce.ui.ListBox 31344 * @extends tinymce.ui.MenuButton 31345 */ 31346 define("tinymce/ui/ListBox", [ 31347 "tinymce/ui/MenuButton" 31348 ], function(MenuButton) { 31349 "use strict"; 31350 31351 return MenuButton.extend({ 31352 /** 31353 * Constructs a instance with the specified settings. 31354 * 31355 * @constructor 31356 * @param {Object} settings Name/value object with settings. 31357 * @setting {Array} values Array with values to add to list box. 31358 */ 31359 init: function(settings) { 31360 var self = this, values, i, selected, selectedText, lastItemCtrl; 31361 31362 self._values = values = settings.values; 31363 if (values) { 31364 for (i = 0; i < values.length; i++) { 31365 selected = values[i].selected || settings.value === values[i].value; 31366 31367 if (selected) { 31368 selectedText = selectedText || values[i].text; 31369 self._value = values[i].value; 31370 break; 31371 } 31372 } 31373 31374 // Default with first item 31375 if (!selected && values.length > 0) { 31376 selectedText = values[0].text; 31377 self._value = values[0].value; 31378 } 31379 31380 settings.menu = values; 31381 } 31382 31383 settings.text = settings.text || selectedText || values[0].text; 31384 31385 self._super(settings); 31386 self.addClass('listbox'); 31387 31388 self.on('select', function(e) { 31389 var ctrl = e.control; 31390 31391 if (lastItemCtrl) { 31392 e.lastControl = lastItemCtrl; 31393 } 31394 31395 if (settings.multiple) { 31396 ctrl.active(!ctrl.active()); 31397 } else { 31398 self.value(e.control.settings.value); 31399 } 31400 31401 lastItemCtrl = ctrl; 31402 }); 31403 }, 31404 31405 /** 31406 * Getter/setter function for the control value. 31407 * 31408 * @method value 31409 * @param {String} [value] Value to be set. 31410 * @return {Boolean/tinymce.ui.ListBox} Value or self if it's a set operation. 31411 */ 31412 value: function(value) { 31413 var self = this, active, selectedText, menu, i; 31414 31415 function activateByValue(menu, value) { 31416 menu.items().each(function(ctrl) { 31417 active = ctrl.value() === value; 31418 31419 if (active) { 31420 selectedText = selectedText || ctrl.text(); 31421 } 31422 31423 ctrl.active(active); 31424 31425 if (ctrl.menu) { 31426 activateByValue(ctrl.menu, value); 31427 } 31428 }); 31429 } 31430 31431 if (typeof(value) != "undefined") { 31432 if (self.menu) { 31433 activateByValue(self.menu, value); 31434 } else { 31435 menu = self.settings.menu; 31436 for (i = 0; i < menu.length; i++) { 31437 active = menu[i].value == value; 31438 31439 if (active) { 31440 selectedText = selectedText || menu[i].text; 31441 } 31442 31443 menu[i].active = active; 31444 } 31445 } 31446 31447 self.text(selectedText || this.settings.text); 31448 } 31449 31450 return self._super(value); 31451 } 31452 }); 31453 }); 31454 31455 // Included from: js/tinymce/classes/ui/MenuItem.js 31456 31457 /** 31458 * MenuItem.js 31459 * 31460 * Copyright, Moxiecode Systems AB 31461 * Released under LGPL License. 31462 * 31463 * License: http://www.tinymce.com/license 31464 * Contributing: http://www.tinymce.com/contributing 31465 */ 31466 31467 /** 31468 * Creates a new menu item. 31469 * 31470 * @-x-less MenuItem.less 31471 * @class tinymce.ui.MenuItem 31472 * @extends tinymce.ui.Control 31473 */ 31474 define("tinymce/ui/MenuItem", [ 31475 "tinymce/ui/Widget", 31476 "tinymce/ui/Factory", 31477 "tinymce/Env" 31478 ], function(Widget, Factory, Env) { 31479 "use strict"; 31480 31481 return Widget.extend({ 31482 Defaults: { 31483 border: 0, 31484 role: 'menuitem' 31485 }, 31486 31487 /** 31488 * Constructs a instance with the specified settings. 31489 * 31490 * @constructor 31491 * @param {Object} settings Name/value object with settings. 31492 * @setting {Boolean} selectable Selectable menu. 31493 * @setting {Array} menu Submenu array with items. 31494 * @setting {String} shortcut Shortcut to display for menu item. Example: Ctrl+X 31495 */ 31496 init: function(settings) { 31497 var self = this; 31498 31499 self.hasPopup = true; 31500 31501 self._super(settings); 31502 31503 settings = self.settings; 31504 31505 self.addClass('menu-item'); 31506 31507 if (settings.menu) { 31508 self.addClass('menu-item-expand'); 31509 } 31510 31511 if (settings.preview) { 31512 self.addClass('menu-item-preview'); 31513 } 31514 31515 if (self._text === '-' || self._text === '|') { 31516 self.addClass('menu-item-sep'); 31517 self.aria('role', 'separator'); 31518 self._text = '-'; 31519 } 31520 31521 if (settings.selectable) { 31522 self.aria('role', 'menuitemcheckbox'); 31523 self.addClass('menu-item-checkbox'); 31524 settings.icon = 'selected'; 31525 } 31526 31527 if (!settings.preview && !settings.selectable) { 31528 self.addClass('menu-item-normal'); 31529 } 31530 31531 self.on('mousedown', function(e) { 31532 e.preventDefault(); 31533 }); 31534 31535 if (settings.menu && !settings.ariaHideMenu) { 31536 self.aria('haspopup', true); 31537 } 31538 }, 31539 31540 /** 31541 * Returns true/false if the menuitem has sub menu. 31542 * 31543 * @method hasMenus 31544 * @return {Boolean} True/false state if it has submenu. 31545 */ 31546 hasMenus: function() { 31547 return !!this.settings.menu; 31548 }, 31549 31550 /** 31551 * Shows the menu for the menu item. 31552 * 31553 * @method showMenu 31554 */ 31555 showMenu: function() { 31556 var self = this, settings = self.settings, menu, parent = self.parent(); 31557 31558 parent.items().each(function(ctrl) { 31559 if (ctrl !== self) { 31560 ctrl.hideMenu(); 31561 } 31562 }); 31563 31564 if (settings.menu) { 31565 menu = self.menu; 31566 31567 if (!menu) { 31568 menu = settings.menu; 31569 31570 // Is menu array then auto constuct menu control 31571 if (menu.length) { 31572 menu = { 31573 type: 'menu', 31574 items: menu 31575 }; 31576 } else { 31577 menu.type = menu.type || 'menu'; 31578 } 31579 31580 if (parent.settings.itemDefaults) { 31581 menu.itemDefaults = parent.settings.itemDefaults; 31582 } 31583 31584 menu = self.menu = Factory.create(menu).parent(self).renderTo(); 31585 menu.reflow(); 31586 menu.fire('show'); 31587 menu.on('cancel', function(e) { 31588 e.stopPropagation(); 31589 self.focus(); 31590 menu.hide(); 31591 }); 31592 31593 menu.on('hide', function(e) { 31594 if (e.control === menu) { 31595 self.removeClass('selected'); 31596 } 31597 }); 31598 31599 menu.submenu = true; 31600 } else { 31601 menu.show(); 31602 } 31603 31604 menu._parentMenu = parent; 31605 31606 menu.addClass('menu-sub'); 31607 31608 var rel = menu.testMoveRel( 31609 self.getEl(), 31610 self.isRtl() ? ['tl-tr', 'bl-br', 'tr-tl', 'br-bl'] : ['tr-tl', 'br-bl', 'tl-tr', 'bl-br'] 31611 ); 31612 31613 menu.moveRel(self.getEl(), rel); 31614 menu.rel = rel; 31615 31616 rel = 'menu-sub-' + rel; 31617 menu.removeClass(menu._lastRel); 31618 menu.addClass(rel); 31619 menu._lastRel = rel; 31620 31621 self.addClass('selected'); 31622 self.aria('expanded', true); 31623 } 31624 }, 31625 31626 /** 31627 * Hides the menu for the menu item. 31628 * 31629 * @method hideMenu 31630 */ 31631 hideMenu: function() { 31632 var self = this; 31633 31634 if (self.menu) { 31635 self.menu.items().each(function(item) { 31636 if (item.hideMenu) { 31637 item.hideMenu(); 31638 } 31639 }); 31640 31641 self.menu.hide(); 31642 self.aria('expanded', false); 31643 } 31644 31645 return self; 31646 }, 31647 31648 /** 31649 * Renders the control as a HTML string. 31650 * 31651 * @method renderHtml 31652 * @return {String} HTML representing the control. 31653 */ 31654 renderHtml: function() { 31655 var self = this, id = self._id, settings = self.settings, prefix = self.classPrefix, text = self.encode(self._text); 31656 var icon = self.settings.icon, image = '', shortcut = settings.shortcut; 31657 31658 if (icon) { 31659 self.parent().addClass('menu-has-icons'); 31660 } 31661 31662 if (settings.image) { 31663 icon = 'none'; 31664 image = ' style="background-image: url(\'' + settings.image + '\')"'; 31665 } 31666 31667 if (shortcut && Env.mac) { 31668 // format shortcut for Mac 31669 shortcut = shortcut.replace(/ctrl\+alt\+/i, '⌥⌘'); // ctrl+cmd 31670 shortcut = shortcut.replace(/ctrl\+/i, '⌘'); // ctrl symbol 31671 shortcut = shortcut.replace(/alt\+/i, '⌥'); // cmd symbol 31672 shortcut = shortcut.replace(/shift\+/i, '⇧'); // shift symbol 31673 } 31674 31675 icon = prefix + 'ico ' + prefix + 'i-' + (self.settings.icon || 'none'); 31676 31677 return ( 31678 '<div id="' + id + '" class="' + self.classes() + '" tabindex="-1">' + 31679 (text !== '-' ? '<i class="' + icon + '"' + image + '></i>\u00a0' : '') + 31680 (text !== '-' ? '<span id="' + id + '-text" class="' + prefix + 'text">' + text + '</span>' : '') + 31681 (shortcut ? '<div id="' + id + '-shortcut" class="' + prefix + 'menu-shortcut">' + shortcut + '</div>' : '') + 31682 (settings.menu ? '<div class="' + prefix + 'caret"></div>' : '') + 31683 '</div>' 31684 ); 31685 }, 31686 31687 /** 31688 * Gets invoked after the control has been rendered. 31689 * 31690 * @method postRender 31691 */ 31692 postRender: function() { 31693 var self = this, settings = self.settings; 31694 31695 var textStyle = settings.textStyle; 31696 if (typeof(textStyle) == "function") { 31697 textStyle = textStyle.call(this); 31698 } 31699 31700 if (textStyle) { 31701 var textElm = self.getEl('text'); 31702 if (textElm) { 31703 textElm.setAttribute('style', textStyle); 31704 } 31705 } 31706 31707 self.on('mouseenter click', function(e) { 31708 if (e.control === self) { 31709 if (!settings.menu && e.type === 'click') { 31710 self.fire('select'); 31711 self.parent().hideAll(); 31712 } else { 31713 self.showMenu(); 31714 31715 if (e.aria) { 31716 self.menu.focus(true); 31717 } 31718 } 31719 } 31720 }); 31721 31722 self._super(); 31723 31724 return self; 31725 }, 31726 31727 active: function(state) { 31728 if (typeof(state) != "undefined") { 31729 this.aria('checked', state); 31730 } 31731 31732 return this._super(state); 31733 }, 31734 31735 /** 31736 * Removes the control and it's menus. 31737 * 31738 * @method remove 31739 */ 31740 remove: function() { 31741 this._super(); 31742 31743 if (this.menu) { 31744 this.menu.remove(); 31745 } 31746 } 31747 }); 31748 }); 31749 31750 // Included from: js/tinymce/classes/ui/Menu.js 31751 31752 /** 31753 * Menu.js 31754 * 31755 * Copyright, Moxiecode Systems AB 31756 * Released under LGPL License. 31757 * 31758 * License: http://www.tinymce.com/license 31759 * Contributing: http://www.tinymce.com/contributing 31760 */ 31761 31762 /** 31763 * Creates a new menu. 31764 * 31765 * @-x-less Menu.less 31766 * @class tinymce.ui.Menu 31767 * @extends tinymce.ui.FloatPanel 31768 */ 31769 define("tinymce/ui/Menu", [ 31770 "tinymce/ui/FloatPanel", 31771 "tinymce/ui/MenuItem", 31772 "tinymce/util/Tools" 31773 ], function(FloatPanel, MenuItem, Tools) { 31774 "use strict"; 31775 31776 var Menu = FloatPanel.extend({ 31777 Defaults: { 31778 defaultType: 'menuitem', 31779 border: 1, 31780 layout: 'stack', 31781 role: 'application', 31782 bodyRole: 'menu', 31783 ariaRoot: true 31784 }, 31785 31786 /** 31787 * Constructs a instance with the specified settings. 31788 * 31789 * @constructor 31790 * @param {Object} settings Name/value object with settings. 31791 */ 31792 init: function(settings) { 31793 var self = this; 31794 31795 settings.autohide = true; 31796 settings.constrainToViewport = true; 31797 31798 if (settings.itemDefaults) { 31799 var items = settings.items, i = items.length; 31800 31801 while (i--) { 31802 items[i] = Tools.extend({}, settings.itemDefaults, items[i]); 31803 } 31804 } 31805 31806 self._super(settings); 31807 self.addClass('menu'); 31808 }, 31809 31810 /** 31811 * Repaints the control after a layout operation. 31812 * 31813 * @method repaint 31814 */ 31815 repaint: function() { 31816 this.toggleClass('menu-align', true); 31817 31818 this._super(); 31819 31820 this.getEl().style.height = ''; 31821 this.getEl('body').style.height = ''; 31822 31823 return this; 31824 }, 31825 31826 /** 31827 * Hides/closes the menu. 31828 * 31829 * @method cancel 31830 */ 31831 cancel: function() { 31832 var self = this; 31833 31834 self.hideAll(); 31835 self.fire('select'); 31836 }, 31837 31838 /** 31839 * Hide menu and all sub menus. 31840 * 31841 * @method hideAll 31842 */ 31843 hideAll: function() { 31844 var self = this; 31845 31846 this.find('menuitem').exec('hideMenu'); 31847 31848 return self._super(); 31849 }, 31850 /* 31851 getContainerElm: function() { 31852 var doc = document, id = this.classPrefix + 'menucontainer'; 31853 31854 var elm = doc.getElementById(id); 31855 if (!elm) { 31856 elm = doc.createElement('div'); 31857 elm.id = id; 31858 elm.setAttribute('role', 'application'); 31859 elm.className = this.classPrefix + '-reset'; 31860 elm.style.position = 'absolute'; 31861 elm.style.top = elm.style.left = '0'; 31862 elm.style.overflow = 'visible'; 31863 doc.body.appendChild(elm); 31864 } 31865 31866 return elm; 31867 }, 31868 */ 31869 /** 31870 * Invoked before the menu is rendered. 31871 * 31872 * @method preRender 31873 */ 31874 preRender: function() { 31875 var self = this; 31876 31877 self.items().each(function(ctrl) { 31878 var settings = ctrl.settings; 31879 31880 if (settings.icon || settings.selectable) { 31881 self._hasIcons = true; 31882 return false; 31883 } 31884 }); 31885 31886 return self._super(); 31887 } 31888 }); 31889 31890 return Menu; 31891 }); 31892 31893 // Included from: js/tinymce/classes/ui/Radio.js 31894 31895 /** 31896 * Radio.js 31897 * 31898 * Copyright, Moxiecode Systems AB 31899 * Released under LGPL License. 31900 * 31901 * License: http://www.tinymce.com/license 31902 * Contributing: http://www.tinymce.com/contributing 31903 */ 31904 31905 /** 31906 * Creates a new radio button. 31907 * 31908 * @-x-less Radio.less 31909 * @class tinymce.ui.Radio 31910 * @extends tinymce.ui.Checkbox 31911 */ 31912 define("tinymce/ui/Radio", [ 31913 "tinymce/ui/Checkbox" 31914 ], function(Checkbox) { 31915 "use strict"; 31916 31917 return Checkbox.extend({ 31918 Defaults: { 31919 classes: "radio", 31920 role: "radio" 31921 } 31922 }); 31923 }); 31924 31925 // Included from: js/tinymce/classes/ui/ResizeHandle.js 31926 31927 /** 31928 * ResizeHandle.js 31929 * 31930 * Copyright, Moxiecode Systems AB 31931 * Released under LGPL License. 31932 * 31933 * License: http://www.tinymce.com/license 31934 * Contributing: http://www.tinymce.com/contributing 31935 */ 31936 31937 /** 31938 * Renders a resize handle that fires ResizeStart, Resize and ResizeEnd events. 31939 * 31940 * @-x-less ResizeHandle.less 31941 * @class tinymce.ui.ResizeHandle 31942 * @extends tinymce.ui.Widget 31943 */ 31944 define("tinymce/ui/ResizeHandle", [ 31945 "tinymce/ui/Widget", 31946 "tinymce/ui/DragHelper" 31947 ], function(Widget, DragHelper) { 31948 "use strict"; 31949 31950 return Widget.extend({ 31951 /** 31952 * Renders the control as a HTML string. 31953 * 31954 * @method renderHtml 31955 * @return {String} HTML representing the control. 31956 */ 31957 renderHtml: function() { 31958 var self = this, prefix = self.classPrefix; 31959 31960 self.addClass('resizehandle'); 31961 31962 if (self.settings.direction == "both") { 31963 self.addClass('resizehandle-both'); 31964 } 31965 31966 self.canFocus = false; 31967 31968 return ( 31969 '<div id="' + self._id + '" class="' + self.classes() + '">' + 31970 '<i class="' + prefix + 'ico ' + prefix + 'i-resize"></i>' + 31971 '</div>' 31972 ); 31973 }, 31974 31975 /** 31976 * Called after the control has been rendered. 31977 * 31978 * @method postRender 31979 */ 31980 postRender: function() { 31981 var self = this; 31982 31983 self._super(); 31984 31985 self.resizeDragHelper = new DragHelper(this._id, { 31986 start: function() { 31987 self.fire('ResizeStart'); 31988 }, 31989 31990 drag: function(e) { 31991 if (self.settings.direction != "both") { 31992 e.deltaX = 0; 31993 } 31994 31995 self.fire('Resize', e); 31996 }, 31997 31998 stop: function() { 31999 self.fire('ResizeEnd'); 32000 } 32001 }); 32002 }, 32003 32004 remove: function() { 32005 if (this.resizeDragHelper) { 32006 this.resizeDragHelper.destroy(); 32007 } 32008 32009 return this._super(); 32010 } 32011 }); 32012 }); 32013 32014 // Included from: js/tinymce/classes/ui/Spacer.js 32015 32016 /** 32017 * Spacer.js 32018 * 32019 * Copyright, Moxiecode Systems AB 32020 * Released under LGPL License. 32021 * 32022 * License: http://www.tinymce.com/license 32023 * Contributing: http://www.tinymce.com/contributing 32024 */ 32025 32026 /** 32027 * Creates a spacer. This control is used in flex layouts for example. 32028 * 32029 * @-x-less Spacer.less 32030 * @class tinymce.ui.Spacer 32031 * @extends tinymce.ui.Widget 32032 */ 32033 define("tinymce/ui/Spacer", [ 32034 "tinymce/ui/Widget" 32035 ], function(Widget) { 32036 "use strict"; 32037 32038 return Widget.extend({ 32039 /** 32040 * Renders the control as a HTML string. 32041 * 32042 * @method renderHtml 32043 * @return {String} HTML representing the control. 32044 */ 32045 renderHtml: function() { 32046 var self = this; 32047 32048 self.addClass('spacer'); 32049 self.canFocus = false; 32050 32051 return '<div id="' + self._id + '" class="' + self.classes() + '"></div>'; 32052 } 32053 }); 32054 }); 32055 32056 // Included from: js/tinymce/classes/ui/SplitButton.js 32057 32058 /** 32059 * SplitButton.js 32060 * 32061 * Copyright, Moxiecode Systems AB 32062 * Released under LGPL License. 32063 * 32064 * License: http://www.tinymce.com/license 32065 * Contributing: http://www.tinymce.com/contributing 32066 */ 32067 32068 /** 32069 * Creates a split button. 32070 * 32071 * @-x-less SplitButton.less 32072 * @class tinymce.ui.SplitButton 32073 * @extends tinymce.ui.Button 32074 */ 32075 define("tinymce/ui/SplitButton", [ 32076 "tinymce/ui/MenuButton", 32077 "tinymce/ui/DomUtils" 32078 ], function(MenuButton, DomUtils) { 32079 return MenuButton.extend({ 32080 Defaults: { 32081 classes: "widget btn splitbtn", 32082 role: "button" 32083 }, 32084 32085 /** 32086 * Repaints the control after a layout operation. 32087 * 32088 * @method repaint 32089 */ 32090 repaint: function() { 32091 var self = this, elm = self.getEl(), rect = self.layoutRect(), mainButtonElm, menuButtonElm; 32092 32093 self._super(); 32094 32095 mainButtonElm = elm.firstChild; 32096 menuButtonElm = elm.lastChild; 32097 32098 DomUtils.css(mainButtonElm, { 32099 width: rect.w - DomUtils.getSize(menuButtonElm).width, 32100 height: rect.h - 2 32101 }); 32102 32103 DomUtils.css(menuButtonElm, { 32104 height: rect.h - 2 32105 }); 32106 32107 return self; 32108 }, 32109 32110 /** 32111 * Sets the active menu state. 32112 * 32113 * @private 32114 */ 32115 activeMenu: function(state) { 32116 var self = this; 32117 32118 DomUtils.toggleClass(self.getEl().lastChild, self.classPrefix + 'active', state); 32119 }, 32120 32121 /** 32122 * Renders the control as a HTML string. 32123 * 32124 * @method renderHtml 32125 * @return {String} HTML representing the control. 32126 */ 32127 renderHtml: function() { 32128 var self = this, id = self._id, prefix = self.classPrefix; 32129 var icon = self.settings.icon ? prefix + 'ico ' + prefix + 'i-' + self.settings.icon : ''; 32130 32131 return ( 32132 '<div id="' + id + '" class="' + self.classes() + '" role="button" tabindex="-1">' + 32133 '<button type="button" hidefocus="1" tabindex="-1">' + 32134 (icon ? '<i class="' + icon + '"></i>' : '') + 32135 (self._text ? (icon ? ' ' : '') + self._text : '') + 32136 '</button>' + 32137 '<button type="button" class="' + prefix + 'open" hidefocus="1" tabindex="-1">' + 32138 //(icon ? '<i class="' + icon + '"></i>' : '') + 32139 (self._menuBtnText ? (icon ? '\u00a0' : '') + self._menuBtnText : '') + 32140 ' <i class="' + prefix + 'caret"></i>' + 32141 '</button>' + 32142 '</div>' 32143 ); 32144 }, 32145 32146 /** 32147 * Called after the control has been rendered. 32148 * 32149 * @method postRender 32150 */ 32151 postRender: function() { 32152 var self = this, onClickHandler = self.settings.onclick; 32153 32154 self.on('click', function(e) { 32155 var node = e.target; 32156 32157 if (e.control == this) { 32158 // Find clicks that is on the main button 32159 while (node) { 32160 if ((e.aria && e.aria.key != 'down') || (node.nodeName == 'BUTTON' && node.className.indexOf('open') == -1)) { 32161 e.stopImmediatePropagation(); 32162 onClickHandler.call(this, e); 32163 return; 32164 } 32165 32166 node = node.parentNode; 32167 } 32168 } 32169 }); 32170 32171 delete self.settings.onclick; 32172 32173 return self._super(); 32174 } 32175 }); 32176 }); 32177 32178 // Included from: js/tinymce/classes/ui/StackLayout.js 32179 32180 /** 32181 * StackLayout.js 32182 * 32183 * Copyright, Moxiecode Systems AB 32184 * Released under LGPL License. 32185 * 32186 * License: http://www.tinymce.com/license 32187 * Contributing: http://www.tinymce.com/contributing 32188 */ 32189 32190 /** 32191 * This layout uses the browsers layout when the items are blocks. 32192 * 32193 * @-x-less StackLayout.less 32194 * @class tinymce.ui.StackLayout 32195 * @extends tinymce.ui.FlowLayout 32196 */ 32197 define("tinymce/ui/StackLayout", [ 32198 "tinymce/ui/FlowLayout" 32199 ], function(FlowLayout) { 32200 "use strict"; 32201 32202 return FlowLayout.extend({ 32203 Defaults: { 32204 containerClass: 'stack-layout', 32205 controlClass: 'stack-layout-item', 32206 endClass : 'break' 32207 } 32208 }); 32209 }); 32210 32211 // Included from: js/tinymce/classes/ui/TabPanel.js 32212 32213 /** 32214 * TabPanel.js 32215 * 32216 * Copyright, Moxiecode Systems AB 32217 * Released under LGPL License. 32218 * 32219 * License: http://www.tinymce.com/license 32220 * Contributing: http://www.tinymce.com/contributing 32221 */ 32222 32223 /** 32224 * Creates a tab panel control. 32225 * 32226 * @-x-less TabPanel.less 32227 * @class tinymce.ui.TabPanel 32228 * @extends tinymce.ui.Panel 32229 * 32230 * @setting {Number} activeTab Active tab index. 32231 */ 32232 define("tinymce/ui/TabPanel", [ 32233 "tinymce/ui/Panel", 32234 "tinymce/ui/DomUtils" 32235 ], function(Panel, DomUtils) { 32236 "use strict"; 32237 32238 return Panel.extend({ 32239 lastIdx: 0, 32240 32241 Defaults: { 32242 layout: 'absolute', 32243 defaults: { 32244 type: 'panel' 32245 } 32246 }, 32247 32248 /** 32249 * Activates the specified tab by index. 32250 * 32251 * @method activateTab 32252 * @param {Number} idx Index of the tab to activate. 32253 */ 32254 activateTab: function(idx) { 32255 var activeTabElm; 32256 32257 if (this.activeTabId) { 32258 activeTabElm = this.getEl(this.activeTabId); 32259 DomUtils.removeClass(activeTabElm, this.classPrefix + 'active'); 32260 activeTabElm.setAttribute('aria-selected', "false"); 32261 } 32262 32263 this.activeTabId = 't' + idx; 32264 32265 activeTabElm = this.getEl('t' + idx); 32266 activeTabElm.setAttribute('aria-selected', "true"); 32267 DomUtils.addClass(activeTabElm, this.classPrefix + 'active'); 32268 32269 if (idx != this.lastIdx) { 32270 this.items()[this.lastIdx].hide(); 32271 this.lastIdx = idx; 32272 } 32273 32274 this.items()[idx].show().fire('showtab'); 32275 this.reflow(); 32276 }, 32277 32278 /** 32279 * Renders the control as a HTML string. 32280 * 32281 * @method renderHtml 32282 * @return {String} HTML representing the control. 32283 */ 32284 renderHtml: function() { 32285 var self = this, layout = self._layout, tabsHtml = '', prefix = self.classPrefix; 32286 32287 self.preRender(); 32288 layout.preRender(self); 32289 32290 self.items().each(function(ctrl, i) { 32291 var id = self._id + '-t' + i; 32292 32293 ctrl.aria('role', 'tabpanel'); 32294 ctrl.aria('labelledby', id); 32295 32296 tabsHtml += ( 32297 '<div id="' + id + '" class="' + prefix + 'tab" ' + 32298 'unselectable="on" role="tab" aria-controls="' + ctrl._id + '" aria-selected="false" tabIndex="-1">' + 32299 self.encode(ctrl.settings.title) + 32300 '</div>' 32301 ); 32302 }); 32303 32304 return ( 32305 '<div id="' + self._id + '" class="' + self.classes() + '" hidefocus="1" tabindex="-1">' + 32306 '<div id="' + self._id + '-head" class="' + prefix + 'tabs" role="tablist">' + 32307 tabsHtml + 32308 '</div>' + 32309 '<div id="' + self._id + '-body" class="' + self.classes('body') + '">' + 32310 layout.renderHtml(self) + 32311 '</div>' + 32312 '</div>' 32313 ); 32314 }, 32315 32316 /** 32317 * Called after the control has been rendered. 32318 * 32319 * @method postRender 32320 */ 32321 postRender: function() { 32322 var self = this; 32323 32324 self._super(); 32325 32326 self.settings.activeTab = self.settings.activeTab || 0; 32327 self.activateTab(self.settings.activeTab); 32328 32329 this.on('click', function(e) { 32330 var targetParent = e.target.parentNode; 32331 32332 if (e.target.parentNode.id == self._id + '-head') { 32333 var i = targetParent.childNodes.length; 32334 32335 while (i--) { 32336 if (targetParent.childNodes[i] == e.target) { 32337 self.activateTab(i); 32338 } 32339 } 32340 } 32341 }); 32342 }, 32343 32344 /** 32345 * Initializes the current controls layout rect. 32346 * This will be executed by the layout managers to determine the 32347 * default minWidth/minHeight etc. 32348 * 32349 * @method initLayoutRect 32350 * @return {Object} Layout rect instance. 32351 */ 32352 initLayoutRect: function() { 32353 var self = this, rect, minW, minH; 32354 32355 minW = DomUtils.getSize(self.getEl('head')).width; 32356 minW = minW < 0 ? 0 : minW; 32357 minH = 0; 32358 self.items().each(function(item, i) { 32359 minW = Math.max(minW, item.layoutRect().minW); 32360 minH = Math.max(minH, item.layoutRect().minH); 32361 if (self.settings.activeTab != i) { 32362 item.hide(); 32363 } 32364 }); 32365 32366 self.items().each(function(ctrl) { 32367 ctrl.settings.x = 0; 32368 ctrl.settings.y = 0; 32369 ctrl.settings.w = minW; 32370 ctrl.settings.h = minH; 32371 32372 ctrl.layoutRect({ 32373 x: 0, 32374 y: 0, 32375 w: minW, 32376 h: minH 32377 }); 32378 }); 32379 32380 var headH = DomUtils.getSize(self.getEl('head')).height; 32381 32382 self.settings.minWidth = minW; 32383 self.settings.minHeight = minH + headH; 32384 32385 rect = self._super(); 32386 rect.deltaH += headH; 32387 rect.innerH = rect.h - rect.deltaH; 32388 32389 return rect; 32390 } 32391 }); 32392 }); 32393 32394 // Included from: js/tinymce/classes/ui/TextBox.js 32395 32396 /** 32397 * TextBox.js 32398 * 32399 * Copyright, Moxiecode Systems AB 32400 * Released under LGPL License. 32401 * 32402 * License: http://www.tinymce.com/license 32403 * Contributing: http://www.tinymce.com/contributing 32404 */ 32405 32406 /** 32407 * Creates a new textbox. 32408 * 32409 * @-x-less TextBox.less 32410 * @class tinymce.ui.TextBox 32411 * @extends tinymce.ui.Widget 32412 */ 32413 define("tinymce/ui/TextBox", [ 32414 "tinymce/ui/Widget", 32415 "tinymce/ui/DomUtils" 32416 ], function(Widget, DomUtils) { 32417 "use strict"; 32418 32419 return Widget.extend({ 32420 /** 32421 * Constructs a instance with the specified settings. 32422 * 32423 * @constructor 32424 * @param {Object} settings Name/value object with settings. 32425 * @setting {Boolean} multiline True if the textbox is a multiline control. 32426 * @setting {Number} maxLength Max length for the textbox. 32427 * @setting {Number} size Size of the textbox in characters. 32428 */ 32429 init: function(settings) { 32430 var self = this; 32431 32432 self._super(settings); 32433 32434 self._value = settings.value || ''; 32435 self.addClass('textbox'); 32436 32437 if (settings.multiline) { 32438 self.addClass('multiline'); 32439 } else { 32440 // TODO: Rework this 32441 self.on('keydown', function(e) { 32442 if (e.keyCode == 13) { 32443 self.parents().reverse().each(function(ctrl) { 32444 e.preventDefault(); 32445 32446 if (ctrl.hasEventListeners('submit') && ctrl.toJSON) { 32447 ctrl.fire('submit', {data: ctrl.toJSON()}); 32448 return false; 32449 } 32450 }); 32451 } 32452 }); 32453 } 32454 }, 32455 32456 /** 32457 * Getter/setter function for the disabled state. 32458 * 32459 * @method value 32460 * @param {Boolean} [state] State to be set. 32461 * @return {Boolean|tinymce.ui.ComboBox} True/false or self if it's a set operation. 32462 */ 32463 disabled: function(state) { 32464 var self = this; 32465 32466 if (self._rendered && typeof(state) != 'undefined') { 32467 self.getEl().disabled = state; 32468 } 32469 32470 return self._super(state); 32471 }, 32472 32473 /** 32474 * Getter/setter function for the control value. 32475 * 32476 * @method value 32477 * @param {String} [value] Value to be set. 32478 * @return {String|tinymce.ui.ComboBox} Value or self if it's a set operation. 32479 */ 32480 value: function(value) { 32481 var self = this; 32482 32483 if (typeof(value) != "undefined") { 32484 self._value = value; 32485 32486 if (self._rendered) { 32487 self.getEl().value = value; 32488 } 32489 32490 return self; 32491 } 32492 32493 if (self._rendered) { 32494 return self.getEl().value; 32495 } 32496 32497 return self._value; 32498 }, 32499 32500 /** 32501 * Repaints the control after a layout operation. 32502 * 32503 * @method repaint 32504 */ 32505 repaint: function() { 32506 var self = this, style, rect, borderBox, borderW = 0, borderH = 0, lastRepaintRect; 32507 32508 style = self.getEl().style; 32509 rect = self._layoutRect; 32510 lastRepaintRect = self._lastRepaintRect || {}; 32511 32512 // Detect old IE 7+8 add lineHeight to align caret vertically in the middle 32513 var doc = document; 32514 if (!self.settings.multiline && doc.all && (!doc.documentMode || doc.documentMode <= 8)) { 32515 style.lineHeight = (rect.h - borderH) + 'px'; 32516 } 32517 32518 borderBox = self._borderBox; 32519 borderW = borderBox.left + borderBox.right + 8; 32520 borderH = borderBox.top + borderBox.bottom + (self.settings.multiline ? 8 : 0); 32521 32522 if (rect.x !== lastRepaintRect.x) { 32523 style.left = rect.x + 'px'; 32524 lastRepaintRect.x = rect.x; 32525 } 32526 32527 if (rect.y !== lastRepaintRect.y) { 32528 style.top = rect.y + 'px'; 32529 lastRepaintRect.y = rect.y; 32530 } 32531 32532 if (rect.w !== lastRepaintRect.w) { 32533 style.width = (rect.w - borderW) + 'px'; 32534 lastRepaintRect.w = rect.w; 32535 } 32536 32537 if (rect.h !== lastRepaintRect.h) { 32538 style.height = (rect.h - borderH) + 'px'; 32539 lastRepaintRect.h = rect.h; 32540 } 32541 32542 self._lastRepaintRect = lastRepaintRect; 32543 self.fire('repaint', {}, false); 32544 32545 return self; 32546 }, 32547 32548 /** 32549 * Renders the control as a HTML string. 32550 * 32551 * @method renderHtml 32552 * @return {String} HTML representing the control. 32553 */ 32554 renderHtml: function() { 32555 var self = this, id = self._id, settings = self.settings, value = self.encode(self._value, false), extraAttrs = ''; 32556 32557 if ("spellcheck" in settings) { 32558 extraAttrs += ' spellcheck="' + settings.spellcheck + '"'; 32559 } 32560 32561 if (settings.maxLength) { 32562 extraAttrs += ' maxlength="' + settings.maxLength + '"'; 32563 } 32564 32565 if (settings.size) { 32566 extraAttrs += ' size="' + settings.size + '"'; 32567 } 32568 32569 if (settings.subtype) { 32570 extraAttrs += ' type="' + settings.subtype + '"'; 32571 } 32572 32573 if (self.disabled()) { 32574 extraAttrs += ' disabled="disabled"'; 32575 } 32576 32577 if (settings.multiline) { 32578 return ( 32579 '<textarea id="' + id + '" class="' + self.classes() + '" ' + 32580 (settings.rows ? ' rows="' + settings.rows + '"' : '') + 32581 ' hidefocus="1"' + extraAttrs + '>' + value + 32582 '</textarea>' 32583 ); 32584 } 32585 32586 return '<input id="' + id + '" class="' + self.classes() + '" value="' + value + '" hidefocus="1"' + extraAttrs + ' />'; 32587 }, 32588 32589 /** 32590 * Called after the control has been rendered. 32591 * 32592 * @method postRender 32593 */ 32594 postRender: function() { 32595 var self = this; 32596 32597 DomUtils.on(self.getEl(), 'change', function(e) { 32598 self.fire('change', e); 32599 }); 32600 32601 return self._super(); 32602 }, 32603 32604 remove: function() { 32605 DomUtils.off(this.getEl()); 32606 this._super(); 32607 } 32608 }); 32609 }); 32610 32611 // Included from: js/tinymce/classes/ui/Throbber.js 32612 32613 /** 32614 * Throbber.js 32615 * 32616 * Copyright, Moxiecode Systems AB 32617 * Released under LGPL License. 32618 * 32619 * License: http://www.tinymce.com/license 32620 * Contributing: http://www.tinymce.com/contributing 32621 */ 32622 32623 /** 32624 * This class enables you to display a Throbber for any element. 32625 * 32626 * @-x-less Throbber.less 32627 * @class tinymce.ui.Throbber 32628 */ 32629 define("tinymce/ui/Throbber", [ 32630 "tinymce/ui/DomUtils", 32631 "tinymce/ui/Control" 32632 ], function(DomUtils, Control) { 32633 "use strict"; 32634 32635 /** 32636 * Constructs a new throbber. 32637 * 32638 * @constructor 32639 * @param {Element} elm DOM Html element to display throbber in. 32640 * @param {Boolean} inline Optional true/false state if the throbber should be appended to end of element for infinite scroll. 32641 */ 32642 return function(elm, inline) { 32643 var self = this, state, classPrefix = Control.classPrefix; 32644 32645 /** 32646 * Shows the throbber. 32647 * 32648 * @method show 32649 * @param {Number} [time] Time to wait before showing. 32650 * @return {tinymce.ui.Throbber} Current throbber instance. 32651 */ 32652 self.show = function(time) { 32653 self.hide(); 32654 32655 state = true; 32656 32657 window.setTimeout(function() { 32658 if (state) { 32659 elm.appendChild(DomUtils.createFragment( 32660 '<div class="' + classPrefix + 'throbber' + (inline ? ' ' + classPrefix + 'throbber-inline' : '') + '"></div>' 32661 )); 32662 } 32663 }, time || 0); 32664 32665 return self; 32666 }; 32667 32668 /** 32669 * Hides the throbber. 32670 * 32671 * @method hide 32672 * @return {tinymce.ui.Throbber} Current throbber instance. 32673 */ 32674 self.hide = function() { 32675 var child = elm.lastChild; 32676 32677 if (child && child.className.indexOf('throbber') != -1) { 32678 child.parentNode.removeChild(child); 32679 } 32680 32681 state = false; 32682 32683 return self; 32684 }; 32685 }; 32686 }); 32687 32688 expose(["tinymce/dom/Sizzle","tinymce/html/Styles","tinymce/dom/EventUtils","tinymce/dom/TreeWalker","tinymce/util/Tools","tinymce/dom/Range","tinymce/html/Entities","tinymce/Env","tinymce/dom/DOMUtils","tinymce/dom/ScriptLoader","tinymce/AddOnManager","tinymce/html/Node","tinymce/html/Schema","tinymce/html/SaxParser","tinymce/html/DomParser","tinymce/html/Writer","tinymce/html/Serializer","tinymce/dom/Serializer","tinymce/dom/TridentSelection","tinymce/util/VK","tinymce/dom/ControlSelection","tinymce/dom/Selection","tinymce/Formatter","tinymce/UndoManager","tinymce/EnterKey","tinymce/ForceBlocks","tinymce/EditorCommands","tinymce/util/URI","tinymce/util/Class","tinymce/util/EventDispatcher","tinymce/ui/Selector","tinymce/ui/Collection","tinymce/ui/DomUtils","tinymce/ui/Control","tinymce/ui/Factory","tinymce/ui/KeyboardNavigation","tinymce/ui/Container","tinymce/ui/DragHelper","tinymce/ui/Scrollable","tinymce/ui/Panel","tinymce/ui/Movable","tinymce/ui/Resizable","tinymce/ui/FloatPanel","tinymce/ui/Window","tinymce/ui/MessageBox","tinymce/WindowManager","tinymce/util/Quirks","tinymce/util/Observable","tinymce/EditorObservable","tinymce/Shortcuts","tinymce/Editor","tinymce/util/I18n","tinymce/FocusManager","tinymce/EditorManager","tinymce/LegacyInput","tinymce/util/XHR","tinymce/util/JSON","tinymce/util/JSONRequest","tinymce/util/JSONP","tinymce/util/LocalStorage","tinymce/Compat","tinymce/ui/Layout","tinymce/ui/AbsoluteLayout","tinymce/ui/Tooltip","tinymce/ui/Widget","tinymce/ui/Button","tinymce/ui/ButtonGroup","tinymce/ui/Checkbox","tinymce/ui/PanelButton","tinymce/ui/ColorButton","tinymce/ui/ComboBox","tinymce/ui/Path","tinymce/ui/ElementPath","tinymce/ui/FormItem","tinymce/ui/Form","tinymce/ui/FieldSet","tinymce/ui/FilePicker","tinymce/ui/FitLayout","tinymce/ui/FlexLayout","tinymce/ui/FlowLayout","tinymce/ui/FormatControls","tinymce/ui/GridLayout","tinymce/ui/Iframe","tinymce/ui/Label","tinymce/ui/Toolbar","tinymce/ui/MenuBar","tinymce/ui/MenuButton","tinymce/ui/ListBox","tinymce/ui/MenuItem","tinymce/ui/Menu","tinymce/ui/Radio","tinymce/ui/ResizeHandle","tinymce/ui/Spacer","tinymce/ui/SplitButton","tinymce/ui/StackLayout","tinymce/ui/TabPanel","tinymce/ui/TextBox","tinymce/ui/Throbber"]); 32689 })(this);